use std::path::{Path, PathBuf}; use std::sync::Arc; use dunce::canonicalize; use itertools::Itertools; use pathfinder_color::ColorU; use galaxy_core::features::FeatureFlag; use galaxy_core::ui::Icon; use warp_util::path::LineAndColumnArg; use warpui::elements::{ resizable_state_handle, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CrossAxisAlignment, DragBarSide, Element, Empty, Flex, MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, PositionedElementAnchor, Resizable, ResizableStateHandle, Shrinkable, Text, }; use warpui::fonts::{Properties, Weight}; use warpui::keymap::EditableBinding; use warpui::platform::Cursor; use warpui::ui_components::components::UiComponent; use warpui::{ AppContext, Entity, EntityId, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle, }; use crate::ai::agent::AgentReviewCommentBatch; use crate::appearance::{Appearance, AppearanceEvent}; use crate::code::buffer_location::LocalOrRemotePath; use crate::code_review::code_review_header::HEADER_BUTTON_PADDING; #[cfg(feature = "local_fs")] use crate::code_review::code_review_view::CodeReviewAction; use crate::code_review::code_review_view::{ render_file_navigation_button, CodeReviewCommentDebugState, CodeReviewView, CodeReviewViewEvent, ReviewActionTargetProvider, CONTENT_LEFT_MARGIN, CONTENT_RIGHT_MARGIN, }; use crate::code_review::diff_state::DiffStateModel; use crate::code_review::telemetry_event::CodeReviewContextDestination; use crate::drive::panel::{MAX_SIDEBAR_WIDTH_RATIO, MIN_SIDEBAR_WIDTH}; use crate::pane_group::pane::view::header::components::HEADER_EDGE_PADDING; use crate::pane_group::pane::view::header::PANE_HEADER_HEIGHT; use crate::pane_group::{ Event as PaneGroupEvent, PaneGroup, WorkingDirectoriesEvent, WorkingDirectoriesModel, }; use crate::settings::{AISettings, AISettingsChangedEvent}; use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel; use crate::terminal::input::MenuPositioning; use crate::terminal::resizable_data::{ModalType, ResizableData}; use crate::terminal::view::TerminalView; use crate::terminal::CLIAgent; use crate::ui_components::buttons::icon_button_with_color; use crate::ui_components::icons; use crate::util::bindings::{keybinding_name_to_display_string, CustomAction}; #[cfg(feature = "local_fs")] use crate::util::openable_file_type::FileTarget; use crate::util::path::{display_name_with_host, display_path_with_host}; use crate::view_components::action_button::{ActionButton, PaneHeaderTheme}; #[cfg(feature = "local_fs")] use crate::view_components::action_button::{NakedTheme, TooltipAlignment}; use crate::view_components::{Dropdown, DropdownItem}; use crate::workspace::view::TOGGLE_RIGHT_PANEL_BINDING_NAME; use crate::workspace::WorkspaceAction; /// Describes which agent destination is available for sending review comments. #[derive(Clone, Debug, PartialEq)] pub enum ReviewDestination { /// No terminal is available to receive comments. None, /// A Warp agent terminal is available (input box visible, not executing). Warp, /// A CLI agent (e.g. Claude Code, Gemini) is running in a terminal. Cli(CLIAgent), } /// Result of attempting to submit review comments to a terminal. pub enum ReviewSubmissionResult { Success { comment_count: usize, file_count: usize, destination: CodeReviewContextDestination, }, Error, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ReviewTerminalUnavailableReason { NoSelectedRepo, SessionPathUnavailable, SessionOutsideSelectedRepo, AIDisabled, TerminalExecuting, InputBoxNotVisible, } impl ReviewTerminalUnavailableReason { fn label(&self) -> &'static str { match self { Self::NoSelectedRepo => "no repo is selected for code review", Self::SessionPathUnavailable => "session cwd is unavailable or not local", Self::SessionOutsideSelectedRepo => "session cwd is not inside selected repo", Self::AIDisabled => "AI is disabled for Galaxy review destinations", Self::TerminalExecuting => "terminal is currently executing a command", Self::InputBoxNotVisible => "terminal input box is not visible", } } } #[derive(Debug)] struct ReviewTerminalStatus { active_session_path: Option, current_repo_path: Option, active_cli_agent: Option, is_executing: bool, is_input_box_visible: bool, unavailable_reasons: Vec, } impl ReviewTerminalStatus { fn is_available(&self) -> bool { self.unavailable_reasons.is_empty() } } /// `ReviewActionTargetProvider` backed by the right panel's active pane group, /// so code review actions resolve their target terminal at action time instead /// of using a handle captured when the review view was created. struct RightPanelReviewActionTargetProvider { right_panel: WeakViewHandle, } impl ReviewActionTargetProvider for RightPanelReviewActionTargetProvider { fn attach_terminal( &self, repo_path: &LocalOrRemotePath, app: &AppContext, ) -> Option> { let right_panel = self.right_panel.upgrade(app)?; right_panel.read(app, |panel, app| { let pane_group = panel.active_pane_group.as_ref()?; let ai_enabled = AISettings::as_ref(app).is_any_ai_enabled(app); panel .find_review_terminal(pane_group, repo_path, ai_enabled, app) .or_else(|| { // No terminal is available (e.g. all candidates are // executing). Fall back to the focused terminal when it is // inside the repo, so per-action handling for busy // terminals still targets the focused conversation. let focused = pane_group .read(app, |pane_group, app| pane_group.focused_session_view(app))?; let status = RightPanelView::review_terminal_status( &focused, Some(repo_path), ai_enabled, app, ); let in_repo = !status.unavailable_reasons.iter().any(|reason| { matches!( reason, ReviewTerminalUnavailableReason::NoSelectedRepo | ReviewTerminalUnavailableReason::SessionPathUnavailable | ReviewTerminalUnavailableReason::SessionOutsideSelectedRepo ) }); in_repo.then_some(focused) }) }) } fn focused_terminal(&self, app: &AppContext) -> Option> { let right_panel = self.right_panel.upgrade(app)?; right_panel.read(app, |panel, app| { let pane_group = panel.active_pane_group.as_ref()?; pane_group.read(app, |pane_group, app| { pane_group .focused_session_view(app) .or_else(|| pane_group.active_session_view(app)) }) }) } } struct CodeReviewState { dropdown: ViewHandle>, available_repos: Vec, /// The repository path of the focused terminal focused_repo_path: Option, /// The repository path of the repository selected in the dropdown selected_repo_path: Option, /// Avoid showing the jump-to-repo button if the focused repo has not changed did_focused_repo_change: bool, } #[cfg(feature = "local_fs")] struct CodeReviewSessionEnv { is_remote: bool, is_wsl: bool, } /// Resolve the repo-switcher dropdown's text color from the current theme. /// Kept as a free function so the construction site and the /// `AppearanceEvent::ThemeChanged` subscription compute the exact same color. fn repo_dropdown_font_color(appearance: &Appearance) -> ColorU { appearance .theme() .sub_text_color(appearance.theme().background()) .into_solid() } impl CodeReviewState { pub fn new(ctx: &mut ViewContext) -> Self { CodeReviewState { dropdown: ctx.add_typed_action_view(|ctx| { let (font_color, ui_font_size) = { let appearance = Appearance::as_ref(ctx); ( repo_dropdown_font_color(appearance), appearance.ui_font_size(), ) }; let mut dropdown = Dropdown::new(ctx); dropdown.set_menu_position( PositionedElementAnchor::BottomRight, ChildAnchor::TopRight, ctx, ); dropdown.set_main_axis_size(MainAxisSize::Min, ctx); dropdown.set_font_color(font_color, ctx); dropdown.set_font_size(ui_font_size, ctx); dropdown.set_vertical_margin(0., ctx); dropdown.set_top_bar_height(galaxy_core::ui::icons::ICON_DIMENSIONS, ctx); dropdown.set_padding(HEADER_BUTTON_PADDING, ctx); // The font color above is derived from the active theme and // cached inside the dropdown. Without this subscription, the // cached value goes stale across light/dark switches and the // header label becomes unreadable on the new background // (e.g. white-on-white in light mode after starting in dark). ctx.subscribe_to_model(&Appearance::handle(ctx), |dropdown, _, event, ctx| { if matches!(event, AppearanceEvent::ThemeChanged) { let font_color = repo_dropdown_font_color(Appearance::as_ref(ctx)); dropdown.set_font_color(font_color, ctx); } }); dropdown }), available_repos: vec![], selected_repo_path: None, focused_repo_path: None, did_focused_repo_change: false, } } #[cfg(not(feature = "local_fs"))] fn set_available_repos( &mut self, _repos: Vec, _ctx: &mut ViewContext, ) { } #[cfg(feature = "local_fs")] fn set_available_repos( &mut self, repos: Vec, ctx: &mut ViewContext, ) { let should_clear = self .selected_repo_path .as_ref() .map(|p| !repos.contains(p)) .unwrap_or(false); if should_clear { self.selected_repo_path = None; } self.available_repos = repos; self.update_repo_dropdown(ctx); // Auto-select first repo if we have one and no selection yet if self.selected_repo_path.is_none() { if let Some(first_repo) = self.available_repos.first() { self.set_selected_repo(first_repo.clone(), ctx); } } } #[cfg(not(feature = "local_fs"))] pub fn set_selected_repo( &mut self, _repo_path: LocalOrRemotePath, _ctx: &mut ViewContext, ) { } #[cfg(feature = "local_fs")] pub fn set_selected_repo( &mut self, repo_path: LocalOrRemotePath, ctx: &mut ViewContext, ) { self.set_selected_repo_internal(repo_path, true, ctx); } pub fn set_focused_repo( &mut self, repo_path: Option, ctx: &mut ViewContext, ) { self.did_focused_repo_change = true; self.focused_repo_path = repo_path; ctx.notify(); } /// Internal method to set the selected repo with control over whether to update the dropdown. /// When `update_dropdown` is false, we skip updating the dropdown (useful when the change /// is coming from the dropdown itself to avoid circular updates). #[cfg(feature = "local_fs")] fn set_selected_repo_internal( &mut self, repo_path: LocalOrRemotePath, update_dropdown: bool, ctx: &mut ViewContext, ) { if repo_path.is_remote() && !FeatureFlag::RemoteCodeReview.is_enabled() { return; } if self.selected_repo_path.as_ref() == Some(&repo_path) { return; } self.did_focused_repo_change = false; self.selected_repo_path = Some(repo_path.clone()); // Only update the dropdown if requested (not when selection came from dropdown itself) if update_dropdown { self.update_repo_dropdown(ctx); } ctx.notify(); } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] fn get_repo_display_name( &self, repo_path: &LocalOrRemotePath, ctx: &AppContext, ) -> Option { let name = display_name_with_host(repo_path, ctx); (!name.is_empty()).then_some(name) } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] fn update_repo_dropdown(&mut self, ctx: &mut ViewContext) { // Collect data before borrowing mutably let (items, selected_display_name) = { let items: Vec> = self .available_repos .iter() .map(|repo_path| { let display_name = self .get_repo_display_name(repo_path, ctx) .unwrap_or_else(|| "Unknown".to_string()); DropdownItem::new( display_name, RightPanelAction::SelectRepo { repo_path: repo_path.clone(), from_dropdown: true, }, ) }) .collect(); let selected_display_name = self .selected_repo_path .as_ref() .and_then(|selected| self.get_repo_display_name(selected, ctx)); (items, selected_display_name) }; // Now update the dropdown if !items.is_empty() { self.dropdown.update(ctx, |dropdown, ctx| { dropdown.set_items(items, ctx); if let Some(display_name) = selected_display_name { dropdown.set_selected_by_name(display_name, ctx); } }); } } } #[derive(Clone, Debug, PartialEq)] #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] pub enum RightPanelAction { ToggleFileSidebar, SelectRepo { repo_path: LocalOrRemotePath, from_dropdown: bool, }, OpenRepository, ToggleMaximize, } #[derive(Clone, Debug)] #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] pub enum RightPanelEvent { ToggleMaximize, #[cfg(feature = "local_fs")] OpenFileWithTarget { path: PathBuf, target: FileTarget, line_col: Option, }, OpenFileInNewTab { path: LocalOrRemotePath, line_and_column: Option, }, #[cfg(not(target_family = "wasm"))] OpenLspLogs { log_path: PathBuf, }, } pub struct RightPanelView { resizable_state_handle: ResizableStateHandle, close_button_mouse_state: MouseStateHandle, file_navigation_button_mouse_state: MouseStateHandle, #[cfg(feature = "local_fs")] open_repository_button: ViewHandle, pub active_pane_group: Option>, #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] working_directories_model: ModelHandle, maximize_button: ViewHandle, code_review_state: Option, #[cfg(feature = "local_fs")] code_review_session_env: Option, is_agent_management_view_open: bool, panel_position: super::PanelPosition, } impl RightPanelView { pub fn init(app: &mut AppContext) { use galaxyui::keymap::macros::*; app.register_editable_bindings([EditableBinding::new( "workspace:toggle_maximize_code_review_panel", "Toggle Maximize Code Review Panel", RightPanelAction::ToggleMaximize, ) .with_enabled(|| cfg!(feature = "local_fs")) .with_context_predicate(id!("RightPanelView")) .with_custom_action(CustomAction::ToggleMaximizePane)]); } pub fn new( working_directories_model: ModelHandle, ctx: &mut ViewContext, ) -> Self { let resizable_data_handle = ResizableData::handle(ctx); let resizable_state_handle = match resizable_data_handle .as_ref(ctx) .get_handle(ctx.window_id(), ModalType::RightPanelWidth) { Some(handle) => handle, None => { log::error!("Couldn't retrieve Right panel resizable state handle."); resizable_state_handle(600.0) } }; let code_review_state = if cfg!(feature = "local_fs") { Some(CodeReviewState::new(ctx)) } else { None }; ctx.subscribe_to_model(&working_directories_model, move |me, _, event, ctx| { me.handle_working_directories_event(event, ctx) }); // Recompute terminal availability when CLI agent sessions start or end. ctx.subscribe_to_model(&CLIAgentSessionsModel::handle(ctx), |me, _, _, ctx| { me.recompute_terminal_availability(ctx); }); // Recompute terminal availability when AI is toggled on or off, so the // send button and tooltip update immediately. ctx.subscribe_to_model(&AISettings::handle(ctx), |me, _, event, ctx| { if matches!(event, AISettingsChangedEvent::IsAnyAIEnabled { .. }) { me.recompute_terminal_availability(ctx); } }); let maximize_button = ctx.add_typed_action_view(|ctx| { let mut button = ActionButton::new("", PaneHeaderTheme) .with_icon(Icon::Maximize) .with_tooltip("Maximize") .with_tooltip_positioning_provider(Arc::new(MenuPositioning::BelowInputBox)) .on_click(|ctx| ctx.dispatch_typed_action(RightPanelAction::ToggleMaximize)); if let Some(keybinding_label) = keybinding_name_to_display_string( "workspace:toggle_maximize_code_review_panel", ctx, ) { button = button.with_tooltip_sublabel(keybinding_label); } button }); #[cfg(feature = "local_fs")] let open_repository_button = ctx.add_typed_action_view(|_| { ActionButton::new("Open repository", NakedTheme) .with_size(crate::view_components::action_button::ButtonSize::Small) .with_tooltip("Navigate to a repo and initialize it for coding") .with_tooltip_alignment(TooltipAlignment::Center) .on_click(|ctx| ctx.dispatch_typed_action(RightPanelAction::OpenRepository)) }); Self { resizable_state_handle, close_button_mouse_state: Default::default(), file_navigation_button_mouse_state: Default::default(), #[cfg(feature = "local_fs")] open_repository_button, active_pane_group: None, working_directories_model, maximize_button, code_review_state, #[cfg(feature = "local_fs")] code_review_session_env: None, is_agent_management_view_open: false, panel_position: super::PanelPosition::Right, } } pub fn set_agent_management_view_open(&mut self, is_open: bool, ctx: &mut ViewContext) { self.is_agent_management_view_open = is_open; ctx.notify(); } pub fn set_panel_position( &mut self, position: super::PanelPosition, ctx: &mut ViewContext, ) { self.panel_position = position; ctx.notify(); } #[cfg(feature = "local_fs")] pub fn update_session_env( &mut self, is_remote: bool, is_wsl: bool, ctx: &mut ViewContext, ) { self.code_review_session_env = Some(CodeReviewSessionEnv { is_remote, is_wsl }); ctx.notify(); } pub fn selected_repo_path(&self) -> Option<&LocalOrRemotePath> { self.code_review_state .as_ref() .and_then(|s| s.selected_repo_path.as_ref()) } #[cfg(feature = "local_fs")] pub fn update_selected_repo( &mut self, repo_path: LocalOrRemotePath, ctx: &mut ViewContext, ) { self.handle_action( &RightPanelAction::SelectRepo { repo_path, from_dropdown: false, }, ctx, ); } fn handle_working_directories_event( &mut self, event: &WorkingDirectoriesEvent, ctx: &mut ViewContext, ) { match event { WorkingDirectoriesEvent::RepositoriesChanged { pane_group_id, repositories, } => { let Some(active_pane_group) = &self.active_pane_group else { return; }; if active_pane_group.id() != *pane_group_id { return; } let old_selected = self .code_review_state .as_ref() .and_then(|s| s.selected_repo_path.clone()); if let Some(state) = self.code_review_state.as_mut() { state.set_available_repos(repositories.clone(), ctx); } let new_selected = self .code_review_state .as_ref() .and_then(|s| s.selected_repo_path.clone()); // Only close the old view if the selection actually changed. if old_selected != new_selected { if let Some(old_path) = &old_selected { self.close_code_review_view(*pane_group_id, old_path, ctx); } } if let Some(path) = &new_selected { self.ensure_code_review_view_exists(path, ctx); } self.recompute_terminal_availability(ctx); ctx.notify(); } WorkingDirectoriesEvent::FocusedRepoChanged { pane_group_id, repository_terminal_map: _, focused_repo, } => { let Some(active_pane_group) = &self.active_pane_group else { return; }; if active_pane_group.id() != *pane_group_id { return; } // When the focused terminal changes repos (via CD or pane focus), // update the dropdown to match the focused terminal's repo if let Some(state) = self.code_review_state.as_mut() { state.set_focused_repo(focused_repo.clone(), ctx); } self.recompute_terminal_availability(ctx); ctx.notify(); } _ => {} } } pub fn set_active_pane_group( &mut self, pane_group: ViewHandle, working_directories_model: &ModelHandle, ctx: &mut ViewContext, ) { let pane_group_id = pane_group.id(); // Subscribe to pane group events so we can recompute terminal // availability when terminal state changes (e.g. command // starts/finishes). ctx.subscribe_to_view(&pane_group, |me, _, event, ctx| { if matches!(event, PaneGroupEvent::TerminalViewStateChanged) { me.recompute_terminal_availability(ctx); } }); self.active_pane_group = Some(pane_group); if let Some(state) = &mut self.code_review_state { let (active_repositories, saved_selection) = working_directories_model.read(ctx, |model, _| { let repos: Vec = model .most_recent_repositories_for_pane_group(pane_group_id) .map(|repos| repos.collect()) .unwrap_or_default(); let saved = model.get_selected_review_repo(pane_group_id).cloned(); (repos, saved) }); // Replace the carried-over selection from a different pane group // with whatever was saved for this pane group (if anything). This // ensures `set_available_repos` either keeps the saved selection // (when it's still in the repo list) or falls back to auto-selecting // the first repo, instead of preserving the previous tab's repo. state.selected_repo_path = saved_selection; state.set_available_repos(active_repositories, ctx); } let selected = self .code_review_state .as_ref() .and_then(|s| s.selected_repo_path.clone()); if let Some(selected) = &selected { self.ensure_code_review_view_exists(selected, ctx); } let is_maximized = self.is_maximized(ctx); self.set_maximized(is_maximized, ctx); ctx.notify(); } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] /// Will only update repo_path if one is not already set pub fn open_code_review( &mut self, repo_path: Option, diff_state_model: ModelHandle, ctx: &mut ViewContext, ) { let Some(repo_dropdown_state) = &mut self.code_review_state else { return; }; let (Some(repo_path), Some(active_pane_group)) = (&repo_path, &self.active_pane_group) else { return; }; if repo_path.is_remote() && !FeatureFlag::RemoteCodeReview.is_enabled() { return; } let pane_group_id = active_pane_group.id(); if repo_dropdown_state.selected_repo_path.is_none() { repo_dropdown_state.set_selected_repo(repo_path.clone(), ctx); } // Check if we already have a cached CodeReviewView let working_directories_model = self.working_directories_model.clone(); let existing_view = working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, repo_path); if let Some(view) = existing_view { view.update(ctx, |view, ctx| { view.on_open(ctx); }); self.recompute_terminal_availability(ctx); } else if let Some(view) = self.create_code_review_view(repo_path, diff_state_model.clone(), pane_group_id, ctx) { view.update(ctx, |view, ctx| { view.on_open(ctx); }); self.recompute_terminal_availability(ctx); }; ctx.notify(); } /// Closes the CodeReviewView for the given pane group and repo path (if any) /// by calling on_close. This stops event subscriptions and background work. fn close_code_review_view( &self, pane_group_id: EntityId, repo_path: &LocalOrRemotePath, ctx: &mut ViewContext, ) { if let Some(code_review_view) = self .working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, repo_path) { code_review_view.update(ctx, |view, ctx| { view.on_close(ctx); }); } } /// Closes the currently active CodeReviewView (if any) by calling on_close. fn close_active_code_review_view(&self, ctx: &mut ViewContext) { let Some(state) = &self.code_review_state else { return; }; let (Some(repo_path), Some(pane_group)) = (&state.selected_repo_path, &self.active_pane_group) else { return; }; self.close_code_review_view(pane_group.id(), repo_path, ctx); } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] pub fn close_code_review(&mut self, ctx: &mut ViewContext) { self.close_active_code_review_view(ctx); // Views are cached in WorkingDirectoriesModel, so we just update the UI state if let Some(code_review_state) = &mut self.code_review_state { code_review_state.selected_repo_path = None; } ctx.notify(); } fn render_repo_dropdown(&self) -> Option> { let Some(state) = &self.code_review_state else { return None; }; if state.available_repos.len() <= 1 { return None; } Some( Container::new( ConstrainedBox::new(ChildView::new(&state.dropdown).finish()) .with_max_width(300.) .finish(), ) .with_margin_right(4.) .finish(), ) } fn close_button(&self, appearance: &Appearance, app: &AppContext) -> Box { let ui_builder = appearance.ui_builder().clone(); let tooltip_keybinding = keybinding_name_to_display_string(TOGGLE_RIGHT_PANEL_BINDING_NAME, app); let tooltip = if let Some(keybinding) = tooltip_keybinding { ui_builder .tool_tip_with_sublabel("Close panel".to_string(), keybinding) .build() .finish() } else { ui_builder .tool_tip("Close panel".to_string()) .build() .finish() }; let icon_color = appearance .theme() .sub_text_color(appearance.theme().background()); icon_button_with_color( appearance, icons::Icon::X, false, self.close_button_mouse_state.clone(), icon_color, ) .with_tooltip(move || tooltip) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(WorkspaceAction::ToggleRightPanel); }) .with_cursor(Cursor::PointingHand) .finish() } fn render_simple_header(&self, close_button: Box) -> Box { let left_spacer = Box::new(Shrinkable::new(1.0, Empty::new().finish())); Container::new( ConstrainedBox::new( Flex::row() .with_child(left_spacer) .with_children(vec![close_button]) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) .with_height(PANE_HEADER_HEIGHT) .finish(), ) .with_padding_left(16.) .with_padding_right(HEADER_EDGE_PADDING) .finish() } fn render_panel_content(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let close_button = self.close_button(appearance, app); let Some(state) = &self.code_review_state else { let simple_header = self.render_simple_header(close_button); return Flex::column() .with_child(simple_header) .with_child( Shrinkable::new(1.0, CodeReviewView::render_loading_state(appearance)).finish(), ) .finish(); }; let selected_repo_path = state.selected_repo_path.as_ref().filter(|repo_path| { if repo_path.is_remote() { FeatureFlag::RemoteCodeReview.is_enabled() } else { state.available_repos.contains(repo_path) } }); let Some(selected_repo_path) = selected_repo_path else { let simple_header = self.render_simple_header(close_button); #[cfg(feature = "local_fs")] let no_repo_body = { let open_repo_button = || Some(ChildView::new(&self.open_repository_button).finish()); if let Some(env) = &self.code_review_session_env { if env.is_remote { // No "Open repository" CTA when the session is remote — the // button navigates to a local folder, which is not meaningful // in a remote session. CodeReviewView::render_remote_state(appearance, None) } else if env.is_wsl { CodeReviewView::render_wsl_state(appearance, open_repo_button()) } else { CodeReviewView::render_not_repo_state(appearance, open_repo_button()) } } else { CodeReviewView::render_not_repo_state(appearance, open_repo_button()) } }; #[cfg(not(feature = "local_fs"))] let no_repo_body = CodeReviewView::render_not_repo_state(appearance, None); return Flex::column() .with_child(simple_header) .with_child(Shrinkable::new(1.0, no_repo_body).finish()) .finish(); }; let current_code_review_view = self.active_pane_group.as_ref().and_then(|pane_group| { let pane_group_id = pane_group.id(); self.working_directories_model .as_ref(app) .get_code_review_view(pane_group_id, selected_repo_path) }); if let Some(code_review_view) = current_code_review_view { let header = if FeatureFlag::GitOperationsInCodeReview.is_enabled() { self.render_header(&code_review_view, appearance, app) } else { self.render_header_legacy(appearance, app) }; let code_review_content = Shrinkable::new(1.0, ChildView::new(&code_review_view).finish()).finish(); Flex::column() .with_child(header) .with_child(code_review_content) .finish() } else { let simple_header = self.render_simple_header(close_button); Flex::column() .with_child(simple_header) .with_child( Shrinkable::new(1.0, CodeReviewView::render_loading_state(appearance)).finish(), ) .finish() } } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] fn render_maximize_pane_button(&self) -> Box { ConstrainedBox::new(ChildView::new(&self.maximize_button).finish()) .with_height(galaxy_core::ui::icons::ICON_DIMENSIONS) .with_width(galaxy_core::ui::icons::ICON_DIMENSIONS) .finish() } fn render_header( &self, code_review_view: &ViewHandle, appearance: &Appearance, app: &AppContext, ) -> Box { let theme = appearance.theme(); let sub_text_color = theme.sub_text_color(theme.background()); let crv = code_review_view.as_ref(app); let repo_path = crv.repo_path(); let branch_name = crv .diff_state_model() .read(app, |model, ctx| model.get_current_branch_name(ctx)); let diff_stats = crv.loaded_diff_stats(); let repo_path_element = repo_path.map(|repo_path| { let display_path = display_path_with_host(repo_path, true, app); Container::new( Text::new_inline( format!("{display_path}:"), appearance.ui_font_family(), appearance.ui_font_size(), ) .with_style(Properties::default().weight(Weight::Semibold)) .with_color(sub_text_color.into()) .finish(), ) .with_margin_right(4.) .finish() }); let branch_name_element = branch_name.map(|name| { Container::new( Text::new_inline(name, appearance.ui_font_family(), appearance.ui_font_size()) .with_style(Properties::default().weight(Weight::Semibold)) .with_color(sub_text_color.into()) .finish(), ) .with_margin_right(8.) .finish() }); let stats_element = diff_stats.map(|stats| CodeReviewView::render_diff_stats(&stats, appearance)); let close_button = self.close_button(appearance, app); let mut left_section = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_size(MainAxisSize::Max); if let Some(repo_path_el) = repo_path_element { left_section.add_child(repo_path_el); } if let Some(branch_el) = branch_name_element { left_section.add_child(Shrinkable::new(100.0, branch_el).finish()); } if let Some(stats) = stats_element { left_section.add_child(stats); } let mut right_section = Vec::new(); if let Some(repo_dropdown) = self.render_repo_dropdown() { right_section.push(repo_dropdown); } right_section.push(self.render_maximize_pane_button()); right_section.push(close_button); Container::new( ConstrainedBox::new( Flex::row() .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Clipped::new(Shrinkable::new(1.0, left_section.finish()).finish()).finish(), ) .with_children(right_section) .finish(), ) .with_height(PANE_HEADER_HEIGHT) .finish(), ) .with_padding_left(CONTENT_LEFT_MARGIN) .with_padding_right(CONTENT_RIGHT_MARGIN) .finish() } /// Legacy header layout: "Code review" title + file nav button. fn render_header_legacy(&self, appearance: &Appearance, app: &AppContext) -> Box { let file_navigation_button = { let current_code_review_view = self .code_review_state .as_ref() .and_then(|state| state.selected_repo_path.as_ref()) .and_then(|repo_path| { self.active_pane_group.as_ref().and_then(|pane_group| { let pane_group_id = pane_group.id(); self.working_directories_model .as_ref(app) .get_code_review_view(pane_group_id, repo_path) }) }); let has_files = current_code_review_view .as_ref() .map(|view: &ViewHandle| view.as_ref(app).has_file_states()) .unwrap_or(false); let file_sidebar_expanded = current_code_review_view .as_ref() .map(|view| view.as_ref(app).file_sidebar_expanded()) .unwrap_or(false); if has_files { Some(render_file_navigation_button( appearance, file_sidebar_expanded, self.file_navigation_button_mouse_state.clone(), |ctx| { ctx.dispatch_typed_action(RightPanelAction::ToggleFileSidebar); }, )) } else { None } }; let theme = appearance.theme(); let sub_text_color = theme.sub_text_color(theme.background()); let title = Shrinkable::new( 1.0, Text::new_inline("Code review".to_string(), appearance.ui_font_family(), 12.) .with_style(Properties::default().weight(Weight::Bold)) .with_color(sub_text_color.into()) .finish(), ) .finish(); let close_button = self.close_button(appearance, app); let mut left_section = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_size(MainAxisSize::Max); let has_nav_button = file_navigation_button.is_some(); if let Some(nav_button) = file_navigation_button { left_section.add_child(nav_button); } left_section.add_child(title); let mut right_section = Vec::new(); if let Some(repo_dropdown) = self.render_repo_dropdown() { right_section.push(repo_dropdown); } right_section.push(self.render_maximize_pane_button()); right_section.push(close_button); let left_padding = if has_nav_button { 12. } else { 16. }; Container::new( ConstrainedBox::new( Flex::row() .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(Box::new(Shrinkable::new(1.0, left_section.finish()))) .with_children(right_section) .finish(), ) .with_height(PANE_HEADER_HEIGHT) .finish(), ) .with_padding_left(left_padding) .with_padding_right(HEADER_EDGE_PADDING) .finish() } pub fn set_maximized(&mut self, is_maximized: bool, ctx: &mut ViewContext) { let (icon, tooltip) = if is_maximized { (Icon::Minimize, "Minimize") } else { (Icon::Maximize, "Maximize") }; self.maximize_button.update(ctx, |button, ctx| { let mut new_button = ActionButton::new("", PaneHeaderTheme) .with_icon(icon) .with_tooltip(tooltip) .with_tooltip_positioning_provider(Arc::new(MenuPositioning::BelowInputBox)) .on_click(|ctx| { ctx.dispatch_typed_action(RightPanelAction::ToggleMaximize); }); if let Some(keybinding_label) = keybinding_name_to_display_string( "workspace:toggle_maximize_code_review_panel", ctx, ) { new_button = new_button.with_tooltip_sublabel(keybinding_label); } *button = new_button; ctx.notify(); }); // Propagate maximize state to the active code review view's file sidebar if let Some(code_review_view) = self.get_active_code_review_view(ctx) { code_review_view.update(ctx, |view, ctx| { view.handle_maximization_toggle(is_maximized, ctx); }); } } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] pub fn focus_active_code_review_view(&self, ctx: &mut ViewContext) { let Some(state) = &self.code_review_state else { return; }; let Some(selected_repo_path) = &state.selected_repo_path else { return; }; let Some(active_pane_group) = &self.active_pane_group else { return; }; let pane_group_id = active_pane_group.id(); if let Some(code_review_view) = self .working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, selected_repo_path) { ctx.focus(&code_review_view); } } fn get_active_code_review_view(&self, ctx: &AppContext) -> Option> { let state = self.code_review_state.as_ref()?; let selected_repo_path = state.selected_repo_path.as_ref()?; let active_pane_group = self.active_pane_group.as_ref()?; let pane_group_id = active_pane_group.id(); self.working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, selected_repo_path) } fn is_maximized(&self, app: &AppContext) -> bool { self.active_pane_group .as_ref() .map(|pane_group| pane_group.as_ref(app).is_right_panel_maximized) .unwrap_or(false) } fn create_code_review_view( &self, repo_path: &LocalOrRemotePath, diff_state_model: ModelHandle, pane_group_id: EntityId, ctx: &mut ViewContext, ) -> Option> { // Early check: if pane group has no active repositories, don't create a view. // Remote repos require the RemoteCodeReview feature flag; local repos go // through the active-repos check. let has_active_repos = if repo_path.is_remote() { FeatureFlag::RemoteCodeReview.is_enabled() } else { self.working_directories_model .as_ref(ctx) .most_recent_repositories_for_pane_group(pane_group_id) .is_some_and(|mut repos| repos.any(|r| &r == repo_path)) }; if !has_active_repos { return None; } let diff_state_model_clone = diff_state_model.clone(); let code_review_comment_batch = self.working_directories_model .update(ctx, |working_directories, ctx| { working_directories.get_or_create_code_review_comments(repo_path, ctx) }); let action_target_provider: Box = Box::new(RightPanelReviewActionTargetProvider { right_panel: ctx.handle(), }); let code_review_view = ctx.add_typed_action_view(|ctx| { CodeReviewView::new( Some(repo_path.clone()), diff_state_model_clone, code_review_comment_batch, Some(action_target_provider), ctx, ) }); // Store in cache self.working_directories_model.update(ctx, |model, _ctx| { model.store_code_review_view( pane_group_id, repo_path.clone(), code_review_view.clone(), ); }); ctx.subscribe_to_model(&diff_state_model, |_me, _, _event, ctx| { ctx.notify(); }); ctx.subscribe_to_view(&code_review_view, |me, code_review, event, ctx| { match event { CodeReviewViewEvent::ReviewSubmitted => { if me.is_maximized(ctx) { me.handle_action(&RightPanelAction::ToggleMaximize, ctx); } } CodeReviewViewEvent::SubmitReviewComments { comments, repo_path, } => { Self::route_review_comments(me, &code_review, comments.clone(), repo_path, ctx); } #[cfg(feature = "local_fs")] CodeReviewViewEvent::OpenFileWithTarget { path, target, line_col, } => { ctx.emit(RightPanelEvent::OpenFileWithTarget { path: path.clone(), target: target.clone(), line_col: *line_col, }); } CodeReviewViewEvent::OpenFileInNewTab { path, line_and_column, } => { ctx.emit(RightPanelEvent::OpenFileInNewTab { path: path.clone(), line_and_column: *line_and_column, }); } #[cfg(not(target_family = "wasm"))] CodeReviewViewEvent::OpenLspLogs { log_path } => { ctx.emit(RightPanelEvent::OpenLspLogs { log_path: log_path.clone(), }); } _ => {} } ctx.notify(); }); Some(code_review_view) } /// Routes review comments to the best available terminal. /// Tries the preferred terminal first, then falls back to other terminals /// in the same repo working directory. fn route_review_comments( &mut self, code_review_view: &ViewHandle, comments: AgentReviewCommentBatch, repo_path: &LocalOrRemotePath, ctx: &mut ViewContext, ) { let Some(pane_group) = &self.active_pane_group else { code_review_view.update(ctx, |view, ctx| { view.handle_review_submission_result(ReviewSubmissionResult::Error, ctx); }); return; }; let ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); let chosen = self.find_review_terminal(pane_group, repo_path, ai_enabled, ctx); let Some(terminal_view) = chosen else { log::warn!("No available terminal found for submitting review comments"); code_review_view.update(ctx, |view, ctx| { view.handle_review_submission_result(ReviewSubmissionResult::Error, ctx); }); return; }; let comment_count = comments.comments.len(); let file_count = comments .comments .iter() .filter_map(|c| { c.target .absolute_file_path() .map(LocalOrRemotePath::display_path) }) .collect::>() .len(); let active_cli_agent = terminal_view.read(ctx, |t, ctx| t.active_cli_agent(ctx)); let (result, destination) = if active_cli_agent.is_some() { let r = terminal_view.update(ctx, |terminal, ctx| { terminal.send_review_to_cli_agent_or_rich_input(&comments, ctx) }); let dest = if terminal_view.read(ctx, |t, ctx| t.is_cli_agent_rich_input_open(ctx)) { CodeReviewContextDestination::RichInput } else { CodeReviewContextDestination::Pty }; (r, dest) } else { let r = terminal_view.update(ctx, |terminal, ctx| { terminal.send_inline_review(comments, ctx) }); (r, CodeReviewContextDestination::AgentReview) }; if let Err(err) = &result { log::error!("Failed to submit review comments to terminal: {err}"); } let submission_result = if result.is_ok() { ReviewSubmissionResult::Success { comment_count, file_count, destination, } } else { ReviewSubmissionResult::Error }; code_review_view.update(ctx, |view, ctx| { view.handle_review_submission_result(submission_result, ctx); }); } fn format_optional_path(path: Option<&Path>) -> String { path.map(|path| path.display().to_string()) .unwrap_or_else(|| "".to_string()) } fn format_optional_location(path: Option<&LocalOrRemotePath>) -> String { path.map(LocalOrRemotePath::display_path) .unwrap_or_else(|| "".to_string()) } fn review_terminal_status( tv: &ViewHandle, repo_path: Option<&LocalOrRemotePath>, ai_enabled: bool, ctx: &AppContext, ) -> ReviewTerminalStatus { tv.read(ctx, |t, ctx| { let active_session_path = t.active_session_path_if_local(ctx); let current_repo_path = t.current_repo_path().cloned(); let active_cli_agent = t.active_cli_agent(ctx).map(|agent| format!("{agent:?}")); let model = t.model.lock(); let is_executing = model.block_list().active_block().is_executing(); let is_input_box_visible = t.is_input_box_visible(&model, ctx); let mut unavailable_reasons = Vec::new(); match repo_path { Some(repo_path) => match (repo_path, t.current_repo_path()) { (LocalOrRemotePath::Local(repo_path), _) => { match active_session_path.as_ref() { Some(cwd) if canonicalize(cwd) .as_deref() .unwrap_or(cwd) .starts_with(repo_path) => {} Some(_) => unavailable_reasons .push(ReviewTerminalUnavailableReason::SessionOutsideSelectedRepo), None => unavailable_reasons .push(ReviewTerminalUnavailableReason::SessionPathUnavailable), } } (repo_path @ LocalOrRemotePath::Remote(_), Some(current_repo_path)) if repo_path.strip_repo_prefix(current_repo_path).is_some() => {} (LocalOrRemotePath::Remote(_), Some(_)) => unavailable_reasons .push(ReviewTerminalUnavailableReason::SessionOutsideSelectedRepo), (LocalOrRemotePath::Remote(_), None) => unavailable_reasons .push(ReviewTerminalUnavailableReason::SessionPathUnavailable), }, None => unavailable_reasons.push(ReviewTerminalUnavailableReason::NoSelectedRepo), } if active_cli_agent.is_none() { if !ai_enabled { unavailable_reasons.push(ReviewTerminalUnavailableReason::AIDisabled); } if is_executing { unavailable_reasons.push(ReviewTerminalUnavailableReason::TerminalExecuting); } if !is_input_box_visible { unavailable_reasons.push(ReviewTerminalUnavailableReason::InputBoxNotVisible); } } ReviewTerminalStatus { active_session_path, current_repo_path, active_cli_agent, is_executing, is_input_box_visible, unavailable_reasons, } }) } fn log_code_review_debug_state(debug_state: &CodeReviewCommentDebugState) { log::info!( "Active code review view: repo_path={}, has_active_comment_model={}, review_destination={:?}, total_comments={}, sendable_comments={}, is_collapsed={}, is_outdated_section_collapsed={:?}, ai_available={}, ai_enabled={}, send_button_tooltip={}", Self::format_optional_location(debug_state.repo_path.as_ref()), debug_state.has_active_comment_model, debug_state.comment_list.review_destination, debug_state.comment_list.total_comments, debug_state.comment_list.sendable_comments, debug_state.comment_list.is_collapsed, debug_state.comment_list.is_outdated_section_collapsed, debug_state.comment_list.ai_available, debug_state.comment_list.ai_enabled, debug_state.comment_list.send_button_tooltip_text, ); } pub fn log_review_comment_send_status_for_active_tab(&self, ctx: &AppContext) { let selected_repo_path = self.selected_repo_path().cloned(); let ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); let code_review_debug_state = self.get_active_code_review_view(ctx) .map(|code_review_view| { code_review_view.read(ctx, |view, ctx| view.debug_review_comment_state(ctx)) }); let Some(pane_group) = &self.active_pane_group else { log::info!( "Review comment send status for active tab: no active pane group, selected_repo_path={}, ai_enabled={}", Self::format_optional_location(selected_repo_path.as_ref()), ai_enabled, ); if let Some(debug_state) = &code_review_debug_state { Self::log_code_review_debug_state(debug_state); } return; }; let pane_group_id = pane_group.id(); let visible_pane_ids = pane_group.read(ctx, |pane_group, _| pane_group.visible_pane_ids()); let focused_pane_id = pane_group.read(ctx, |pane_group, ctx| pane_group.focused_pane_id(ctx)); let preferred_terminal_id = selected_repo_path.as_ref().and_then(|repo_path| { self.working_directories_model .as_ref(ctx) .get_terminal_id_for_root_path(pane_group_id, repo_path) }); let chosen_terminal_id = selected_repo_path.as_ref().and_then(|repo_path| { self.find_review_terminal(pane_group, repo_path, ai_enabled, ctx) .map(|terminal_view| terminal_view.id()) }); log::info!( "Review comment send status for active tab: pane_group_id={pane_group_id}, selected_repo_path={}, ai_enabled={}, focused_pane_id={focused_pane_id}, preferred_terminal_id={preferred_terminal_id:?}, chosen_terminal_id={chosen_terminal_id:?}, visible_pane_count={}", Self::format_optional_location(selected_repo_path.as_ref()), ai_enabled, visible_pane_ids.len(), ); if let Some(debug_state) = &code_review_debug_state { Self::log_code_review_debug_state(debug_state); } else { log::info!( "No active code review view is associated with the current tab/repo selection" ); } for (index, pane_id) in visible_pane_ids.iter().enumerate() { let is_focused = *pane_id == focused_pane_id; if !pane_id.is_terminal_pane() { log::info!( "Pane #{index}: pane_id={pane_id}, pane_type={}, focused={is_focused}, skipped=not a terminal pane", pane_id.pane_type(), ); continue; } let terminal_view = pane_group.read(ctx, |pane_group, ctx| { pane_group.terminal_view_from_pane_id(*pane_id, ctx) }); let Some(terminal_view) = terminal_view else { log::info!( "Pane #{index}: pane_id={pane_id}, pane_type={}, focused={is_focused}, skipped=terminal view missing", pane_id.pane_type(), ); continue; }; let terminal_id = terminal_view.id(); let terminal_status = Self::review_terminal_status( &terminal_view, selected_repo_path.as_ref(), ai_enabled, ctx, ); let unavailable_reasons = if terminal_status.unavailable_reasons.is_empty() { "".to_string() } else { terminal_status .unavailable_reasons .iter() .map(ReviewTerminalUnavailableReason::label) .join("; ") }; log::info!( "Pane #{index}: pane_id={pane_id}, pane_type={}, terminal_view_id={terminal_id}, focused={is_focused}, preferred={}, chosen={}, available={}, active_session_path={}, current_repo_path={}, active_cli_agent={}, is_executing={}, is_input_box_visible={}, unavailable_reasons={}", pane_id.pane_type(), preferred_terminal_id == Some(terminal_id), chosen_terminal_id == Some(terminal_id), terminal_status.is_available(), Self::format_optional_path(terminal_status.active_session_path.as_deref()), Self::format_optional_location(terminal_status.current_repo_path.as_ref()), terminal_status .active_cli_agent .as_deref() .unwrap_or(""), terminal_status.is_executing, terminal_status.is_input_box_visible, unavailable_reasons, ); } } /// Returns whether a terminal is in the given repo and available to receive /// review comments. A terminal is available if it is not executing a command /// and has its input box visible, OR if it has an active CLI agent /// (CLI agents are long-running commands that accept review input). /// /// When `ai_enabled` is `false`, only terminals with an active CLI agent are /// considered available (non-CLI Warp terminals require AI to be on). fn is_terminal_available_for_review( tv: &ViewHandle, repo_path: &LocalOrRemotePath, ai_enabled: bool, ctx: &AppContext, ) -> bool { Self::review_terminal_status(tv, Some(repo_path), ai_enabled, ctx).is_available() } /// Finds the best terminal to send review comments to. /// Priority: focused terminal > preferred terminal > other terminals with /// matching CWD that are available. fn find_available_terminal_for_review( terminal_views: &[ViewHandle], focused_terminal: Option<&ViewHandle>, preferred_terminal_id: Option, repo_path: &LocalOrRemotePath, ai_enabled: bool, ctx: &AppContext, ) -> Option> { let is_available = |tv: &ViewHandle| { Self::is_terminal_available_for_review(tv, repo_path, ai_enabled, ctx) }; // Try the focused terminal first. if let Some(tv) = focused_terminal { if is_available(tv) { return Some(tv.clone()); } } // Try the preferred (repo-mapped) terminal next. if let Some(preferred_id) = preferred_terminal_id { if let Some(tv) = terminal_views.iter().find(|tv| tv.id() == preferred_id) { if is_available(tv) { return Some(tv.clone()); } } } // Fallback: any terminal in the repo that is available. terminal_views.iter().find(|tv| is_available(tv)).cloned() } /// Finds the best available terminal for review in the given pane group, /// gathering the terminal list, focused terminal, and preferred terminal ID /// before delegating to `find_available_terminal_for_review`. fn find_review_terminal( &self, pane_group: &ViewHandle, repo_path: &LocalOrRemotePath, ai_enabled: bool, ctx: &AppContext, ) -> Option> { let terminal_views = pane_group.read(ctx, |pg, ctx| pg.visible_terminal_views(ctx)); let focused_terminal = pane_group.read(ctx, |pg, ctx| pg.focused_session_view(ctx)); let pane_group_id = pane_group.id(); let preferred_terminal_id = self .working_directories_model .as_ref(ctx) .get_terminal_id_for_root_path(pane_group_id, repo_path); Self::find_available_terminal_for_review( &terminal_views, focused_terminal.as_ref(), preferred_terminal_id, repo_path, ai_enabled, ctx, ) } /// Checks whether any terminal in the pane group is available for input in /// the correct working directory and pushes the result to the active /// CodeReviewView. pub fn recompute_terminal_availability(&self, ctx: &mut ViewContext) { let Some(code_review_view) = self.get_active_code_review_view(ctx) else { return; }; let repo_path = code_review_view.read(ctx, |view, _| view.repo_path().cloned()); let Some(repo_path) = repo_path else { code_review_view.update(ctx, |view, ctx| { view.set_review_destination(ReviewDestination::None, ctx); }); return; }; let Some(pane_group) = &self.active_pane_group else { code_review_view.update(ctx, |view, ctx| { view.set_review_destination(ReviewDestination::None, ctx); }); return; }; let ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); let destination = self .find_review_terminal(pane_group, &repo_path, ai_enabled, ctx) .map(|tv| { tv.read(ctx, |t, ctx| { t.active_cli_agent(ctx) .map(ReviewDestination::Cli) .unwrap_or(ReviewDestination::Warp) }) }) .unwrap_or(ReviewDestination::None); code_review_view.update(ctx, |view, ctx| { view.set_review_destination(destination, ctx); }); } fn ensure_code_review_view_exists( &mut self, repo_path: &LocalOrRemotePath, ctx: &mut ViewContext, ) { if repo_path.is_remote() && !FeatureFlag::RemoteCodeReview.is_enabled() { return; } let Some(pane_group) = &self.active_pane_group else { return; }; let pane_group_id = pane_group.id(); // Only set up subscriptions and diff loading when the panel is visible. // When the panel opens later, open_code_review will call on_open. let is_panel_open = pane_group.as_ref(ctx).right_panel_open; let existing_view = self .working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, repo_path); if let Some(view) = existing_view { if is_panel_open { // on_open is idempotent (guards on is_open), so this is safe for // already-open views and correctly re-opens cached-but-closed ones. view.update(ctx, |view, ctx| { view.on_open(ctx); }); } } else { // Prefer the pane group's active session so the diff request rides // the connection actually showing the review; the manager falls // back to any connected session for the host when unavailable. let preferred_session = pane_group .read(ctx, |pg, ctx| pg.active_session_view(ctx)) .and_then(|tv| tv.as_ref(ctx).active_block_session_id()); let diff_state_model = self.working_directories_model.update(ctx, |model, ctx| { model.get_or_create_diff_state_model(repo_path.clone(), preferred_session, ctx) }); let Some(diff_state_model) = diff_state_model else { return; }; let is_known_repo = self .working_directories_model .as_ref(ctx) .most_recent_repositories_for_pane_group(pane_group_id) .is_some_and(|mut repos| repos.any(|r| &r == repo_path)); // Only create a view when a terminal exists for the repo. The view // resolves its target terminal lazily via `ReviewActionTargetProvider`, // so this is purely a creation gate. let has_review_terminal = if is_known_repo { let Some(terminal_view_id) = self .working_directories_model .as_ref(ctx) .get_terminal_id_for_root_path(pane_group_id, repo_path) else { return; }; ctx.view_with_id::(ctx.window_id(), terminal_view_id) .is_some() } else { // For repos not yet tracked (e.g. remote repos from direct open), // fall back to the active session. pane_group .read(ctx, |pane_group, ctx| pane_group.active_session_view(ctx)) .is_some() }; if has_review_terminal { if let Some(view) = self.create_code_review_view(repo_path, diff_state_model, pane_group_id, ctx) { if is_panel_open { view.update(ctx, |view, ctx| { view.on_open(ctx); }); } } } } } } impl Entity for RightPanelView { type Event = RightPanelEvent; } #[cfg(feature = "local_fs")] impl TypedActionView for RightPanelView { type Action = RightPanelAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { RightPanelAction::ToggleFileSidebar => { if let Some(state) = &self.code_review_state { if let Some(repo_path) = &state.selected_repo_path { if let Some(pane_group) = &self.active_pane_group { let pane_group_id = pane_group.id(); let working_directories_model = self.working_directories_model.clone(); if let Some(code_review_view) = working_directories_model .as_ref(ctx) .get_code_review_view(pane_group_id, repo_path) { code_review_view.update(ctx, |view, ctx| { view.handle_action(&CodeReviewAction::ToggleFileSidebar, ctx); }); } } } } } RightPanelAction::SelectRepo { repo_path, from_dropdown, } => { // Only close the old view if we're actually switching to a different repo. let is_switching = self .code_review_state .as_ref() .and_then(|s| s.selected_repo_path.as_ref()) .is_some_and(|old| old != repo_path); if is_switching { self.close_active_code_review_view(ctx); } if let Some(state) = &mut self.code_review_state { // Don't update dropdown when selection comes from dropdown itself let should_update_dropdown = !from_dropdown; state.set_selected_repo_internal( repo_path.clone(), should_update_dropdown, ctx, ); self.ensure_code_review_view_exists(repo_path, ctx); // Persist the user's manual selection so it can be restored when // they leave this pane group's session and come back. We only // persist explicit `SelectRepo` actions (i.e. dropdown picks or // contextual opens) so that the auto-selected default doesn't // overwrite an earlier manual choice for a different pane group. if let Some(pane_group) = &self.active_pane_group { let pane_group_id = pane_group.id(); let repo_path = repo_path.clone(); self.working_directories_model.update(ctx, |model, _| { model.set_selected_review_repo(pane_group_id, repo_path); }); } ctx.notify(); } } RightPanelAction::ToggleMaximize => { ctx.emit(RightPanelEvent::ToggleMaximize); ctx.notify(); } RightPanelAction::OpenRepository => { if let Some(active_pane_group) = &self.active_pane_group { let terminal_view = active_pane_group.read(ctx, |pane_group, ctx| { pane_group .active_session_id(ctx) .and_then(|id| pane_group.terminal_view_from_pane_id(id, ctx)) }); if let Some(terminal_view) = terminal_view { terminal_view.update(ctx, |terminal, ctx| { terminal.handle_action( &crate::terminal::view::TerminalAction::PickRepoToOpen, ctx, ); }); } } } } } } #[cfg(not(feature = "local_fs"))] impl TypedActionView for RightPanelView { type Action = RightPanelAction; fn handle_action(&mut self, _action: &Self::Action, _ctx: &mut ViewContext) { // No actions when local_fs is disabled } } impl View for RightPanelView { fn ui_name() -> &'static str { "RightPanelView" } fn render(&self, app: &AppContext) -> Box { let panel_content = self.render_panel_content(app); if self.is_maximized(app) { return Shrinkable::new(1.0, panel_content).finish(); } let drag_side = match self.panel_position { super::PanelPosition::Left => DragBarSide::Right, super::PanelPosition::Right => DragBarSide::Left, }; Resizable::new(self.resizable_state_handle.clone(), panel_content) .with_dragbar_side(drag_side) .on_resize(move |ctx, _| { ctx.notify(); }) .with_bounds_callback(Box::new(|window_size| { let min_width = MIN_SIDEBAR_WIDTH; let max_width = window_size.x() * MAX_SIDEBAR_WIDTH_RATIO; (min_width, max_width.max(min_width)) })) .finish() } }