use crate::app::{
    CloseWindow, DecreaseUiScale, IncreaseUiScale, NewWindow, OpenRepository, ResetUiScale,
};
use crate::kit::{Scrollbar, ScrollbarAxis};
use crate::theme::AppTheme;
use crate::ui_scale;
use gitcomet_core::diff::AnnotatedDiffLine;
#[cfg(test)]
use gitcomet_core::diff::annotate_unified;
#[cfg(test)]
use gitcomet_core::domain::RepoStatus;
use gitcomet_core::domain::{
    Branch, Commit, CommitId, DiffArea, DiffTarget, FileStatus, FileStatusKind, Tag,
    UpstreamDivergence,
};
use gitcomet_core::file_diff::FileDiffRow;
use gitcomet_core::process::refresh_git_runtime;
use gitcomet_core::services::{PullMode, RemoteUrlKind, ResetMode};
use gitcomet_state::model::{
    AppNotificationKind, AppState, AuthPromptKind, CloneOpState, CloneOpStatus, DefaultTagType,
    DiagnosticKind, Loadable, RepoId, RepoState, SubmoduleTrustPromptOperation,
};
use gitcomet_state::msg::{Msg, StoreEvent};
use gitcomet_state::session;
use gitcomet_state::store::AppStore;
use gpui::prelude::*;
use gpui::{
    Anchor, Animation, AnimationExt, AnyElement, AnyView, App, Bounds, ClickEvent, CursorStyle,
    Decorations, DispatchPhase, Element, ElementId, Entity, FocusHandle, FontWeight,
    GlobalElementId, InspectorElementId, IsZero, LayoutId, MouseButton, MouseDownEvent,
    MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, ResizeEdge, ScrollHandle,
    ScrollWheelEvent, ShapedLine, SharedString, Size, Style, StyleRefinement, Styled, TextRun,
    Tiling, UniformListScrollHandle, WeakEntity, Window, WindowControlArea, actions, anchored, div,
    fill, point, px, relative, size, uniform_list,
};
use rustc_hash::{FxHashMap, FxHashSet};
#[cfg(test)]
use std::collections::BTreeMap;
use std::hash::Hash;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::time::{Duration, Instant};

const REPO_ACTIVATION_THROTTLE: Duration = Duration::from_secs(5);

/// How long after requesting an interactive move/resize grab a deactivation is
/// still attributed to that grab. Compositors hand over focus within a frame;
/// generous enough for a loaded system, short enough that a genuine alt-tab
/// right after a drag is not mistaken for the grab.
const WINDOW_GRAB_DEACTIVATE_GRACE: Duration = Duration::from_millis(1_500);

/// Upper bound on how long a drag may hold the grab before the re-activation is
/// no longer treated as its echo. Only a safety valve: arming already requires a
/// fresh grab plus a deactivation within [`WINDOW_GRAB_DEACTIVATE_GRACE`].
const WINDOW_GRAB_REACTIVATE_GRACE: Duration = Duration::from_secs(120);

actions!(
    text_input_diff_navigation,
    [
        DiffPrevFile,
        DiffNextFile,
        DiffPrevSearchMatchOrChange,
        DiffNextSearchMatchOrChange,
        TextInputCommitSubmit,
        TextInputDiffPrevFile,
        TextInputDiffNextFile,
        TextInputDiffPrevSearchMatchOrChange,
        TextInputDiffNextSearchMatchOrChange,
        TextInputDiffPrevChange,
        TextInputDiffNextChange,
        OpenActiveViewSearch,
        PopoverPromptDismiss,
        PopoverPromptTabNext,
        PopoverPromptTabPrev,
        TerminalCopy,
        TerminalPaste,
        TerminalSelectAll,
        ToggleCommandPalette,
        CommandPaletteDismiss,
        LocateFileInExplorer,
    ]
);

pub(crate) fn is_diff_shortcut_candidate(keystroke: &gpui::Keystroke) -> bool {
    let key = keystroke.key.as_str();
    let mods = keystroke.modifiers;
    let no_command_modifiers = !mods.control && !mods.alt && !mods.platform && !mods.function;

    (key == "escape" && no_command_modifiers)
        || (mods.secondary() && mods.number_of_modifiers() == 1 && key == "f")
        || (matches!(key, "f1" | "f2" | "f3" | "f4" | "f7") && no_command_modifiers)
        || (key == "space" && no_command_modifiers)
        || (mods.alt
            && !mods.control
            && !mods.platform
            && !mods.function
            && matches!(
                key,
                "e" | "i" | "s" | "w" | "up" | "down" | "left" | "right"
            ))
        || ((mods.control || mods.platform)
            && !mods.alt
            && !mods.function
            && matches!(
                key,
                "1" | "2" | "3" | "a" | "c" | "e" | "s" | "d" | "h" | "u"
            ))
        || (matches!(key, "a" | "b" | "c" | "d") && no_command_modifiers)
}

/// Whether this activation is the tail of a move/resize grab we started, and so
/// must not be treated as the user returning to the app. Always consumes the
/// marker; a drag that outlives [`WINDOW_GRAB_REACTIVATE_GRACE`] falls back to
/// refreshing, which is the conservative direction.
fn consume_window_grab_activation(suppressed_at: &mut Option<Instant>, now: Instant) -> bool {
    match suppressed_at.take() {
        Some(at) => now.saturating_duration_since(at) <= WINDOW_GRAB_REACTIVATE_GRACE,
        None => false,
    }
}

fn repo_activation_msg(
    state: &AppState,
    last_activation_dispatch: &mut FxHashMap<RepoId, Instant>,
    now: Instant,
) -> Option<Msg> {
    let repo_id = state.active_repo?;
    let repo = state.repos.iter().find(|repo| repo.id == repo_id)?;
    if !matches!(repo.open, Loadable::Ready(_)) {
        return None;
    }
    if last_activation_dispatch
        .get(&repo_id)
        .is_some_and(|last| now.saturating_duration_since(*last) < REPO_ACTIVATION_THROTTLE)
    {
        return None;
    }
    last_activation_dispatch.insert(repo_id, now);
    Some(Msg::RepoActivated { repo_id })
}

mod app_model;
mod branch_sidebar;
mod caches;
mod chrome;
pub(crate) mod clone_progress;
mod color;
mod command_palette;
mod commit_message_hover;
mod commit_message_text;
pub(crate) mod components;
mod conflict_markers;
pub(crate) mod conflict_resolver;
mod date_time;
mod diff_navigation;
mod diff_preview;
mod diff_text_model;
mod diff_text_selection;
mod diff_utils;
mod file_diff_display;
mod file_icons;
mod fingerprint;
mod history_graph;
pub(crate) mod history_mode;
mod history_refs_hover;
mod icons;
#[cfg(any(test, target_os = "linux", target_os = "freebsd"))]
mod linux_desktop_integration;
mod markdown_preview;
mod mod_helpers;
mod open_source_licenses_data;
mod panels;
mod panes;
mod patch_split;
mod path_display;
mod perf;
mod permalink;
pub(super) mod platform_open;
mod poller;
mod reflog_panel;
mod repo_open;
pub(crate) mod rows;
mod settings_window;
pub(crate) mod shortcut_labels;
mod sidebar_presentation;
mod splash;
mod state_apply;
mod terminal_alacritty;
mod terminal_panel;
mod terminal_preferences;
#[cfg(test)]
pub(crate) mod test_support;
mod toast_host;
mod tooltip;
mod tooltip_host;
mod update_check;
mod user_survey;
mod word_diff;

use app_model::AppUiModel;
use branch_sidebar::{BranchSection, BranchSidebarRow};
use caches::{
    HistoryBaseCache, HistoryBaseCacheRequest, HistoryBaseRowVm, HistoryCache,
    HistoryCacheBuildRequest, HistoryDecorationCache, HistoryDecorationCacheRequest,
    HistoryDecorationRowVm, HistoryDisplayKey, HistoryRefListItem, HistoryRefListItemKind,
    HistoryStashIdsCache, HistoryTextVm, HistoryWorktreeSummaryCache,
};
use chrome::{TitleBarView, cursor_style_for_resize_edge, resize_edge};
use conflict_resolver::{ConflictPickSide, ConflictResolverViewMode};
#[cfg(test)]
use date_time::format_datetime;
#[cfg(test)]
use date_time::format_datetime_utc;
use date_time::{DateTimeFormat, Timezone, format_datetime_into};
use diff_preview::build_new_file_preview_from_diff;
use patch_split::build_patch_split_rows;
use poller::Poller;
pub(in crate::view) use terminal_preferences::{
    ActionBarTerminalTarget, ExternalTerminalLaunchContext, ExternalTerminalMode,
    TerminalPreferences, launch_external_terminal_from_preferences, parse_terminal_args_multiline,
    resolve_embedded_shell_program,
};
use word_diff::{capped_word_diff_ranges, capped_word_diff_ranges_for_file_diff_texts};

use commit_message_hover::{CommitMessageHoverHost, CommitMessageHoverState};
#[cfg(test)]
use diff_text_model::CachedDiffTextSegment;
use diff_text_model::{CachedDiffStyledText, SyntaxTokenKind};
use diff_text_selection::{
    ConflictRowSelectionTracker, DiffTextSelectionOverlay, DiffTextSelectionTracker,
};
use diff_utils::{
    build_unified_patch_for_hunks, build_unified_patch_for_selected_lines_across_hunks,
    build_unified_patch_for_selected_lines_across_hunks_for_reverse_apply,
    compute_diff_file_for_src_ix, compute_diff_file_stats,
    context_menu_selection_range_from_diff_text, diff_content_text, image_format_for_path,
    parse_unified_hunk_header_for_display, scrollbar_markers_from_flags,
    scrollbar_markers_from_visible_ranges,
};
use file_diff_display::{
    LARGE_DIFF_TEXT_MIN_BYTES, append_diff_display_text_slice, append_file_diff_display_text_slice,
    file_diff_display_len, file_diff_display_text, should_truncate_file_diff_display,
};
use history_refs_hover::{HISTORY_REFS_HOVER_MENU_INVOKER_PREFIX, HistoryRefsHoverHost};
pub(crate) use mod_helpers::TerminalPanelResizeState;
use mod_helpers::*;
pub use mod_helpers::{
    FocusedMergetoolLabels, FocusedMergetoolViewConfig, GitCometView, GitCometViewConfig,
    GitCometViewMode, InitialRepositoryLaunchMode, StartupCrashReport,
};
use panels::{ActionBarView, BottomStatusBarView, PopoverHost, RepoTabsBarView, action_bar_height};
pub(crate) use panes::MainPaneView;
use panes::{
    CollapsedSidebarSection, DetailsPaneInit, DetailsPaneView, HistoryView, ReflogPaneInit,
    ReflogPaneView, SidebarPaneView,
};
pub(crate) use settings_window::{SettingsWindowView, open_settings_window};
use toast_host::ToastHost;
use tooltip::GitCometTooltipExt;
#[cfg(test)]
use tooltip::clear_visible_tooltip_text_for_test;
use tooltip_host::TooltipHost;

#[cfg(test)]
pub(crate) use chrome::window_frame;
use color::with_alpha;
use icons::{svg_icon, svg_spinner};

const HISTORY_COL_BRANCH_PX: f32 = 130.0;
const HISTORY_COL_GRAPH_PX: f32 = 80.0;
const HISTORY_COL_GRAPH_MAX_PX: f32 = 240.0;
const HISTORY_COL_AUTHOR_PX: f32 = 140.0;
const HISTORY_COL_DATE_PX: f32 = 160.0;
const HISTORY_COL_SHA_PX: f32 = 88.0;
const HISTORY_COL_HANDLE_PX: f32 = 8.0;

const HISTORY_COL_BRANCH_MIN_PX: f32 = 60.0;
const HISTORY_COL_BRANCH_MAX_PX: f32 = 320.0;
const HISTORY_COL_GRAPH_MIN_PX: f32 = 44.0;
const HISTORY_COL_AUTHOR_MIN_PX: f32 = 80.0;
const HISTORY_COL_AUTHOR_MAX_PX: f32 = 260.0;
const HISTORY_COL_DATE_MIN_PX: f32 = 110.0;
const HISTORY_COL_DATE_MAX_PX: f32 = 240.0;
const HISTORY_COL_SHA_MIN_PX: f32 = 60.0;
const HISTORY_COL_SHA_MAX_PX: f32 = 160.0;
const HISTORY_COL_MESSAGE_MIN_PX: f32 = 220.0;
const ERROR_BANNER_OVERFLOW_HINT_MIN_LINES: usize = 8;
const ERROR_BANNER_OVERFLOW_HINT_MIN_CHARS: usize = 240;

const HISTORY_GRAPH_COL_GAP_PX: f32 = 16.0;
const HISTORY_GRAPH_MARGIN_X_PX: f32 = 10.0;
/// Corner radius where a graph line turns between columns. Against a 16px column
/// pitch and a 14px half-row this leaves roughly a 10px straight horizontal run
/// per column crossed and 8px of straight vertical below the corner, so the turn
/// reads as a corner rather than as a diagonal.
const HISTORY_GRAPH_ELBOW_RADIUS_PX: f32 = 6.0;

/// Width of the lane-coloured wash at the right edge of the graph column. It
/// fades from transparent into the border on the message cell, tying a commit's
/// dot to its message.
const HISTORY_GRAPH_FADE_WIDTH_PX: f32 = 44.0;
/// Alpha the fade reaches where it meets the message border. Deliberately faint:
/// it runs behind the lane strokes on every row, so anything stronger reads as a
/// selection highlight.
const HISTORY_GRAPH_FADE_ALPHA: f32 = 0.10;
/// Below this much ref-column width the hover branch badge is dropped rather
/// than truncated to an unreadable stub.
const HISTORY_BRANCH_BADGE_MIN_W_PX: f32 = 34.0;
/// Alpha of the hover branch badge. Faint by design -- it is an on-demand hint
/// in a column that otherwise holds solid ref chips, and must not read as one.
const HISTORY_BRANCH_BADGE_ALPHA: f32 = 0.70;
/// Width of the lane-coloured border down the left edge of the message cell.
const HISTORY_MESSAGE_BORDER_W_PX: f32 = 3.0;
/// Vertical inset of that border, so consecutive rows read as separate borders
/// rather than as one continuous stripe down the list.
const HISTORY_MESSAGE_BORDER_INSET_Y_PX: f32 = 3.0;
/// Gap between that border and the message text.
const HISTORY_MESSAGE_BORDER_GAP_PX: f32 = 6.0;

/// Left offset of the message text inside its cell, in design px.
///
/// With the lane border shown the text clears the border by a fixed gap rather
/// than using the cell's own padding — the border would otherwise sit almost
/// against the text. Shared by the commit rows, which paint their text on a
/// canvas, and the two uncommitted-changes rows, which lay theirs out as
/// elements, so the three cannot drift apart.
const fn history_message_text_left_px(show_graph_color_marker: bool) -> f32 {
    if show_graph_color_marker {
        HISTORY_MESSAGE_BORDER_W_PX + HISTORY_MESSAGE_BORDER_GAP_PX
    } else {
        HISTORY_COL_HANDLE_PX / 2.0
    }
}

const PANE_RESIZE_HANDLE_PX: f32 = 8.0;
const PANE_COLLAPSED_PX: f32 = 34.0;
const PANE_COLLAPSE_ANIM_MS: u64 = 120;
/// Fade-in/out duration for the collapsed-sidebar section popover.
const COLLAPSED_POPOVER_FADE_MS: u64 = 110;
const SIDEBAR_MIN_PX: f32 = 200.0;
const DETAILS_MIN_PX: f32 = 280.0;
const MAIN_MIN_PX: f32 = 280.0;

const DIFF_SPLIT_COL_MIN_PX: f32 = 160.0;

const DIFF_TEXT_LAYOUT_CACHE_MAX_ENTRIES: usize = 4000;
const DIFF_TEXT_LAYOUT_CACHE_PRUNE_OVERAGE: usize = 256;
const TOAST_FADE_IN_MS: u64 = 180;
const TOAST_FADE_OUT_MS: u64 = 220;
const TOAST_SLIDE_PX: f32 = 12.0;
const TERMINAL_PANEL_DEFAULT_HEIGHT_PX: f32 = 220.0;
const TERMINAL_PANEL_RESIZE_HANDLE_PX: f32 = 6.0;
pub(crate) const WEBSITE_URL: &str = "https://gitcomet.dev";
pub(crate) const EDITIONS_URL: &str = "https://gitcomet.dev/#editions";
pub(crate) const RELEASES_URL: &str = "https://github.com/Auto-Explore/GitComet/releases";
pub(crate) const DISCORD_URL: &str = "https://discord.com/invite/2ufDGP8RnA";

pub(in crate::view) fn restrict_scroll_to_vertical_axis<E: Styled>(mut element: E) -> E {
    element.style().restrict_scroll_to_axis = Some(true);
    element
}

// Only use these wrappers for views that remain mounted while their parent is mounted.
// Parent-controlled mount/unmount boundaries, like collapsible panes, must rebuild their child.
fn stable_cached_view<V: Render>(view: Entity<V>, style: StyleRefinement) -> AnyElement {
    let view = AnyView::from(view);
    // GPUI's cached mount path skips some test-only debug bounds and paint tracking.
    if cfg!(test) {
        view.into_any_element()
    } else {
        view.cached(style).into_any_element()
    }
}

fn stable_cached_fill_view<V: Render>(view: Entity<V>) -> AnyElement {
    stable_cached_view(view, StyleRefinement::default().size_full())
}

fn stable_cached_fixed_height_view<V: Render>(view: Entity<V>, height: Pixels) -> AnyElement {
    stable_cached_view(
        view,
        StyleRefinement::default().w_full().h(height).flex_none(),
    )
}

fn stable_overlay_view<V: Render>(view: Entity<V>) -> impl IntoElement {
    // Keep overlay hosts uncached. Their paint ranges are recorded after focused
    // TextInput views register platform input handlers, and Wayland text-input
    // replace_text_in_range can trigger a redraw while that handler is
    // temporarily unavailable. Reusing the cached overlay paint range then
    // replays a stale input-handler index and panics inside GPUI reuse_paint.
    div().absolute().top_0().left_0().size_full().child(view)
}

struct UiScaleScrollCapture {
    view: Entity<GitCometView>,
}

impl IntoElement for UiScaleScrollCapture {
    type Element = Self;

    fn into_element(self) -> Self::Element {
        self
    }
}

impl Element for UiScaleScrollCapture {
    type RequestLayoutState = ();
    type PrepaintState = ();

    fn id(&self) -> Option<ElementId> {
        None
    }

    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
        None
    }

    fn request_layout(
        &mut self,
        _id: Option<&GlobalElementId>,
        _inspector_id: Option<&InspectorElementId>,
        window: &mut Window,
        cx: &mut App,
    ) -> (LayoutId, Self::RequestLayoutState) {
        let mut style = Style::default();
        style.size.width = px(0.0).into();
        style.size.height = px(0.0).into();
        (window.request_layout(style, [], cx), ())
    }

    fn prepaint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _inspector_id: Option<&InspectorElementId>,
        _bounds: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        _window: &mut Window,
        _cx: &mut App,
    ) -> Self::PrepaintState {
    }

    fn paint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _inspector_id: Option<&InspectorElementId>,
        _bounds: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        _prepaint: &mut Self::PrepaintState,
        window: &mut Window,
        cx: &mut App,
    ) {
        if !renders_full_chrome(self.view.read(cx).view_mode) {
            return;
        }

        let view = self.view.clone();
        window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
            let zoom_modifier = event.modifiers.secondary() || event.modifiers.control;
            if phase != DispatchPhase::Capture
                || !zoom_modifier
                || event.modifiers.alt
                || event.modifiers.function
            {
                return;
            }

            if !renders_full_chrome(view.read(cx).view_mode) {
                return;
            }

            let delta_y = event.delta.pixel_delta(window.line_height()).y;
            if delta_y.is_zero() {
                return;
            }

            let current = crate::ui_scale::current(cx).percent;
            let next = if delta_y > px(0.0) {
                crate::ui_scale::step_up(current)
            } else {
                crate::ui_scale::step_down(current)
            };

            cx.stop_propagation();
            if next == current {
                return;
            }

            cx.defer(move |cx| {
                crate::app::set_app_ui_scale_percent(cx, next);
            });
        });
    }
}

fn active_diff_target(state: &AppState) -> Option<(RepoId, DiffTarget)> {
    let repo_id = state.active_repo?;
    let repo = state.repos.iter().find(|repo| repo.id == repo_id)?;
    Some((repo_id, repo.diff_state.diff_target.clone()?))
}

fn active_merge_view_target(state: &AppState) -> Option<(RepoId, DiffTarget)> {
    let (repo_id, target) = active_diff_target(state)?;
    let DiffTarget::WorkingTree { path, area } = &target else {
        return None;
    };
    if *area != DiffArea::Unstaged {
        return None;
    }

    let repo = state.repos.iter().find(|repo| repo.id == repo_id)?;
    repo.status_entry_for_path(DiffArea::Unstaged, path)
        .filter(|entry| entry.kind == FileStatusKind::Conflicted && entry.conflict.is_some())?;
    Some((repo_id, target))
}

#[cfg(test)]
pub(in crate::view) fn pane_resize_drag_width_bounds(
    handle: PaneResizeHandle,
    start_sidebar: Pixels,
    start_details: Pixels,
    total_w: Pixels,
    sidebar_collapsed: bool,
    details_collapsed: bool,
) -> (Pixels, Pixels) {
    let (min_width, other_width, other_collapsed) = match handle {
        PaneResizeHandle::Sidebar => (px(SIDEBAR_MIN_PX), start_details, details_collapsed),
        PaneResizeHandle::Details => (px(DETAILS_MIN_PX), start_sidebar, sidebar_collapsed),
    };
    pane_resize_drag_width_bounds_for_other_pane(
        min_width,
        other_width,
        other_collapsed,
        total_w,
        sidebar_collapsed,
        details_collapsed,
    )
}

#[inline]
pub(in crate::view) fn pane_resize_drag_width_bounds_for_other_pane(
    min_width: Pixels,
    other_width: Pixels,
    other_collapsed: bool,
    total_w: Pixels,
    _sidebar_collapsed: bool,
    _details_collapsed: bool,
) -> (Pixels, Pixels) {
    let main_min = px(MAIN_MIN_PX);
    let collapsed_w = px(PANE_COLLAPSED_PX);
    // Both pane resize handles overlay their boundaries and consume no layout width.
    let available_w = total_w - main_min;
    let other_width = if other_collapsed {
        collapsed_w
    } else {
        other_width
    };
    let max_width = (available_w - other_width).max(min_width);
    (min_width, max_width)
}

pub(in crate::view) fn next_pane_resize_drag_width(
    state: &PaneResizeState,
    current_x: Pixels,
    total_w: Pixels,
    sidebar_collapsed: bool,
    details_collapsed: bool,
) -> Pixels {
    let dx = current_x - state.start_x;
    let (min_width, max_width) =
        state.drag_width_bounds(total_w, sidebar_collapsed, details_collapsed);
    (state.start_width + (dx * state.drag_delta_sign))
        .max(min_width)
        .min(max_width)
}

/// Pure helper: compute the next diff-split ratio for a single drag step.
///
/// Returns `None` when the available width is too narrow for two columns
/// (the caller should force 50/50 in that case).
pub(in crate::view) fn next_diff_split_drag_ratio(
    available: Pixels,
    min_col_w: Pixels,
    start_ratio: f32,
    dx: Pixels,
) -> Option<f32> {
    if available <= min_col_w * 2.0 {
        return None;
    }
    let max_left = available - min_col_w;
    let next_left = ((available * start_ratio) + dx)
        .max(min_col_w)
        .min(max_left);
    Some((next_left / available).clamp(0.0, 1.0))
}

/// Returns `(available, min_col_w)` for the diff-split layout given the main
/// pane's content width.  Bundles the handle-width and column-min constants so
/// callers do not need to reference them directly.
#[inline]
pub(in crate::view) fn diff_split_drag_params(main_pane_content_width: Pixels) -> (Pixels, Pixels) {
    let handle_w = px(PANE_RESIZE_HANDLE_PX);
    let min_col_w = px(DIFF_SPLIT_COL_MIN_PX);
    let available = (main_pane_content_width - handle_w).max(px(0.0));
    (available, min_col_w)
}

#[inline]
pub(in crate::view) fn diff_split_column_widths_from_available(
    available: Pixels,
    min_col_w: Pixels,
    ratio: f32,
) -> (Pixels, Pixels) {
    let left_w = if available <= min_col_w * 2.0 {
        available * 0.5
    } else {
        (available * ratio)
            .max(min_col_w)
            .min(available - min_col_w)
    };
    let right_w = available - left_w;
    (left_w, right_w)
}

#[inline]
pub(in crate::view) fn diff_split_column_widths(
    main_pane_content_width: Pixels,
    ratio: f32,
) -> (Pixels, Pixels) {
    let (available, min_col_w) = diff_split_drag_params(main_pane_content_width);
    diff_split_column_widths_from_available(available, min_col_w, ratio)
}

pub(crate) const UI_MONOSPACE_FONT_FAMILY: &str = crate::bundled_fonts::LILEX_FONT_FAMILY;

impl GitCometView {
    pub(in crate::view) fn open_popover_at(
        &mut self,
        kind: PopoverKind,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_refs_hover_host
            .update(cx, |host, cx| host.close(cx));
        self.popover_host.update(cx, |host, cx| {
            host.open_popover_at(kind, anchor, window, cx)
        });
    }

    /// Close the submodule trust popover only while it is showing its pending
    /// spinner for `repo_id` (no trust prompt yet). Used when a background trust
    /// check resolves to a silent proceed or an error, so the spinner does not
    /// linger. A no-op if the user already dismissed it or another popover is up.
    pub(in crate::view) fn close_submodule_trust_spinner(
        &mut self,
        repo_id: RepoId,
        cx: &mut gpui::Context<Self>,
    ) {
        let kind = PopoverKind::submodule(repo_id, SubmodulePopoverKind::TrustConfirm);
        self.popover_host.update(cx, |host, cx| {
            if host.is_kind_open(&kind) {
                host.close_popover(cx);
            }
        });
    }

    pub(in crate::view) fn open_popover_centered(
        &mut self,
        kind: PopoverKind,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_refs_hover_host
            .update(cx, |host, cx| host.close(cx));
        self.popover_host
            .update(cx, |host, cx| host.open_popover_centered(kind, window, cx));
    }

    #[cfg(target_os = "macos")]
    pub(crate) fn open_clone_repository_prompt(
        &mut self,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.open_popover_centered(PopoverKind::CloneRepo, window, cx);
    }

    pub(in crate::view) fn open_popover_for_bounds(
        &mut self,
        kind: PopoverKind,
        anchor_bounds: Bounds<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_refs_hover_host
            .update(cx, |host, cx| host.close(cx));
        self.popover_host.update(cx, |host, cx| {
            host.open_popover_for_bounds(kind, anchor_bounds, window, cx)
        });
    }

    fn open_command_palette(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) {
        self.command_palette_open = true;
        let restore_focus = window
            .focused(cx)
            .or_else(|| self.pre_palette_focus.clone());
        let fallback_focus = self.main_pane.read(cx).diff_panel_focus_handle.clone();
        let has_active_repo = self.active_repo_id().is_some();
        self.command_palette.update(cx, |palette, cx| {
            palette.open(restore_focus, fallback_focus, has_active_repo, window, cx);
        });
    }

    fn close_command_palette(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) {
        self.command_palette_open = false;
        self.command_palette
            .update(cx, |palette, cx| palette.close(window, cx));
    }

    fn command_palette_did_close(
        &mut self,
        command: Option<&str>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.command_palette_open = false;
        if let Some(command) = command {
            self.execute_command(command, Some(window), cx);
        }
    }

    pub(crate) fn toggle_command_palette(
        &mut self,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.command_palette_open {
            self.close_command_palette(window, cx);
        } else {
            self.open_command_palette(window, cx);
        }
    }

    fn execute_command(
        &mut self,
        command_id: &str,
        window: Option<&mut Window>,
        cx: &mut gpui::Context<Self>,
    ) {
        match command_id {
            "new-window" => cx.defer(|cx| cx.dispatch_action(&NewWindow)),
            "open-settings" => cx.defer(crate::view::open_settings_window),
            "quit" => cx.defer(crate::app::quit_app_or_warn),
            "minimize-window" => cx.defer(|cx| {
                if let Some(win) = cx.active_window() {
                    let _ = win.update(cx, |_root, win, _cx| win.minimize_window());
                }
            }),
            "zoom-window" => cx.defer(|cx| {
                if let Some(win) = cx.active_window() {
                    let _ = win.update(cx, |_root, win, _cx| super::app::toggle_window_zoom(win));
                }
            }),
            "toggle-fullscreen" => cx.defer(|cx| {
                if let Some(win) = cx.active_window() {
                    let _ = win.update(cx, |_root, win, _cx| win.toggle_fullscreen());
                }
            }),
            "increase-ui-scale" => cx.defer(|cx| cx.dispatch_action(&IncreaseUiScale)),
            "decrease-ui-scale" => cx.defer(|cx| cx.dispatch_action(&DecreaseUiScale)),
            "reset-ui-scale" => cx.defer(|cx| cx.dispatch_action(&ResetUiScale)),
            "close-window" => cx.defer(|cx| cx.dispatch_action(&CloseWindow)),
            "locate-file-in-explorer" => self.locate_open_file_in_explorer(cx),
            "open-repository" => cx.defer(|cx| cx.dispatch_action(&OpenRepository)),
            "switch-repository" => {
                if let Some(window) = window {
                    self.open_repository_switcher_centered(window, cx);
                }
            }
            "clone-repository" => {
                if let Some(window) = window {
                    self.open_popover_centered(PopoverKind::CloneRepo, window, cx);
                }
            }
            "close-repo-tab" => {
                self.close_active_repo_tab(cx);
            }
            "reload-repository" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::ReloadRepo { repo_id });
                }
            }
            "fetch-all" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::FetchAll { repo_id });
                }
            }
            "previous-repo-tab" => {
                self.activate_previous_repo_tab(cx);
            }
            "next-repo-tab" => {
                self.activate_next_repo_tab(cx);
            }
            "open-active-view-search" => cx.defer(|cx| cx.dispatch_action(&OpenActiveViewSearch)),
            "toggle-sidebar" => {
                self.set_sidebar_collapsed(!self.sidebar_collapsed, cx);
            }
            "toggle-details" => {
                self.set_details_collapsed(!self.details_collapsed, cx);
            }
            "toggle-diff-view" => {
                let next = match self.diff_view_mode {
                    DiffViewMode::Split => DiffViewMode::Inline,
                    DiffViewMode::Inline => DiffViewMode::Split,
                };
                self.set_diff_view_mode(next, cx);
            }
            "toggle-diff-word-wrap" => {
                self.set_diff_word_wrap(!self.diff_word_wrap, cx);
            }
            "toggle-line-numbers" => {
                self.set_diff_show_line_numbers(!self.diff_show_line_numbers, cx);
            }
            "toggle-whitespace-chars" => {
                self.set_diff_reveal_whitespace_chars(!self.diff_reveal_whitespace_chars, cx);
            }
            "create-branch" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    let target = self
                        .state
                        .repos
                        .iter()
                        .find(|r| r.id == repo_id)
                        .and_then(|repo| {
                            if let Loadable::Ready(head) = &repo.head_branch {
                                Some(head.clone())
                            } else {
                                None
                            }
                        })
                        .unwrap_or_else(|| "HEAD".to_string());
                    self.open_popover_centered(
                        PopoverKind::CreateBranchFromRefPrompt {
                            repo_id,
                            target,
                            source_selectable: true,
                            name_prefix: String::new(),
                        },
                        window,
                        cx,
                    );
                }
            }
            "checkout-branch" => {
                if let Some(window) = window {
                    self.open_popover_centered(
                        PopoverKind::BranchPicker {
                            purpose: BranchPickerPurpose::Checkout,
                        },
                        window,
                        cx,
                    );
                }
            }
            "delete-branch" => {
                if let Some(window) = window {
                    self.open_popover_centered(
                        PopoverKind::BranchPicker {
                            purpose: BranchPickerPurpose::Delete,
                        },
                        window,
                        cx,
                    );
                }
            }
            "rename-branch" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                    && let Some(name) = self
                        .state
                        .repos
                        .iter()
                        .find(|repo| repo.id == repo_id)
                        .and_then(|repo| match &repo.head_branch {
                            Loadable::Ready(name) if name != "HEAD" && !name.is_empty() => {
                                Some(name.clone())
                            }
                            _ => None,
                        })
                {
                    self.open_popover_centered(
                        PopoverKind::RenameBranchPrompt {
                            repo_id,
                            name,
                            is_current_branch: true,
                        },
                        window,
                        cx,
                    );
                }
            }
            "checkout-remote-branch" => {
                // TODO: Open remote branch picker
            }
            "pull" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::Pull {
                        repo_id,
                        mode: PullMode::Default,
                    });
                }
            }
            "push" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::Push { repo_id });
                }
            }
            "force-push" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(
                        PopoverKind::ForcePushConfirm { repo_id },
                        window,
                        cx,
                    );
                }
            }
            "delete-remote-branch" => {
                // TODO: Implement delete remote branch
            }
            "commit" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(PopoverKind::CommitPrompt { repo_id }, window, cx);
                }
            }
            "apply-patch" => {
                let Some(repo_id) = self.active_repo_id() else {
                    return;
                };
                let view = cx.weak_entity();
                cx.defer(move |cx| {
                    let rx = cx.prompt_for_paths(gpui::PathPromptOptions {
                        files: true,
                        directories: false,
                        multiple: false,
                        prompt: Some("Select patch file".into()),
                    });
                    cx.spawn(async move |cx| {
                        let result = rx.await;
                        let paths = match result {
                            Ok(Ok(Some(paths))) => paths,
                            _ => return,
                        };
                        let Some(patch) = paths.into_iter().next() else {
                            return;
                        };
                        let _ = view.update(cx, |this, _cx| {
                            this.store.dispatch(Msg::ApplyPatch { repo_id, patch });
                        });
                    })
                    .detach();
                });
            }
            "stage-all" => {
                let Some(repo_id) = self.active_repo_id() else {
                    return;
                };
                let paths: Vec<_> = self
                    .state
                    .repos
                    .iter()
                    .find(|r| r.id == repo_id)
                    .and_then(|repo| repo.worktree_status_entries())
                    .map(|entries| entries.iter().map(|e| e.path.clone()).collect::<Vec<_>>())
                    .unwrap_or_default();
                if paths.is_empty() {
                    return;
                }
                // Staging is what marks a conflict resolved, so confirm first if
                // any of it still has conflict markers in the worktree. With no
                // window there is nothing to confirm in, and staging unasked is
                // the one outcome this must not have.
                // No row selection is involved here, so there is none to consume.
                if let Some(confirm) = crate::view::conflict_markers::stage_confirm_popover(
                    &self.state,
                    repo_id,
                    paths.clone(),
                    false,
                ) {
                    if let Some(window) = window {
                        self.open_popover_centered(confirm, window, cx);
                    }
                    return;
                }
                self.store.dispatch(Msg::StagePaths {
                    repo_id,
                    paths: paths.into(),
                });
            }
            "unstage-all" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(repo) = self.state.repos.iter().find(|r| r.id == repo_id)
                {
                    let paths: Vec<_> = repo
                        .staged_status_entries()
                        .map(|entries| entries.iter().map(|e| e.path.clone()).collect::<Vec<_>>())
                        .unwrap_or_default();
                    if !paths.is_empty() {
                        self.store.dispatch(Msg::UnstagePaths {
                            repo_id,
                            paths: paths.into(),
                        });
                    }
                }
            }
            "discard-all" => {
                // TODO: Implement discard all changes command
            }
            "stash" => {
                if let Some(window) = window {
                    self.open_popover_centered(PopoverKind::StashPrompt, window, cx);
                }
            }
            "stash-pop" | "stash-apply" | "stash-drop" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    let purpose = match command_id {
                        "stash-pop" => StashPickerPurpose::Pop,
                        "stash-apply" => StashPickerPurpose::Apply,
                        _ => StashPickerPurpose::Drop,
                    };
                    self.open_popover_centered(
                        PopoverKind::StashPickerPrompt { repo_id, purpose },
                        window,
                        cx,
                    );
                }
            }
            "merge" => {
                // TODO: Implement merge branch/ref
            }
            "rebase" => {
                if let Some(window) = window {
                    self.open_popover_centered(
                        PopoverKind::BranchPicker {
                            purpose: BranchPickerPurpose::RebaseOnto,
                        },
                        window,
                        cx,
                    );
                }
            }
            "create-tag" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(
                        PopoverKind::CreateTagPrompt {
                            repo_id,
                            target: "HEAD".into(),
                        },
                        window,
                        cx,
                    );
                }
            }
            "delete-tag" => {
                // TODO: Implement delete tag
            }
            "show-reflog" => {
                self.open_reflog_panel_for_active_repo(cx);
            }
            "add-remote" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(
                        PopoverKind::Repo {
                            repo_id,
                            kind: RepoPopoverKind::Remote(RemotePopoverKind::AddPrompt),
                        },
                        window,
                        cx,
                    );
                }
            }
            "remove-remote" => {
                // TODO: Implement remove remote
            }
            "edit-remote-url" => {
                // TODO: Implement edit remote URL
            }
            "add-submodule" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(
                        PopoverKind::Repo {
                            repo_id,
                            kind: RepoPopoverKind::Submodule(SubmodulePopoverKind::AddPrompt),
                        },
                        window,
                        cx,
                    );
                }
            }
            "update-submodules" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::UpdateSubmodules { repo_id });
                }
            }
            "remove-submodule" => {
                // TODO: Implement remove submodule
            }
            "add-worktree" => {
                if let Some(repo_id) = self.active_repo_id()
                    && let Some(window) = window
                {
                    self.open_popover_centered(
                        PopoverKind::Repo {
                            repo_id,
                            kind: RepoPopoverKind::Worktree(WorktreePopoverKind::AddPrompt),
                        },
                        window,
                        cx,
                    );
                }
            }
            "remove-worktree" => {
                // TODO: Implement remove worktree
            }
            "blame" => {
                self.set_annotate_enabled(!self.annotate_enabled, cx);
            }
            "back" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::GlobalNavBack { repo_id });
                }
            }
            "forward" => {
                if let Some(repo_id) = self.active_repo_id() {
                    self.store.dispatch(Msg::GlobalNavForward { repo_id });
                }
            }
            _ => {}
        }
    }

    /// Whether a popover, dialog, prompt, or context menu is currently open
    /// (all are tracked as a `PopoverKind` by the popover host).
    pub(in crate::view) fn is_overlay_open(&self, cx: &App) -> bool {
        // The collapsed-sidebar section popover covers the history view too, so it
        // must suppress ref hovers the same way the popover host does.
        self.popover_host.read(cx).is_open() || self.sidebar_collapsed_popover.is_some()
    }

    pub(in crate::view) fn show_history_refs_hover(
        &mut self,
        repo_id: RepoId,
        commit_id: CommitId,
        source_bounds: Bounds<Pixels>,
        items: Arc<[HistoryRefListItem]>,
        pointer: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        // Don't surface the refs hover while an overlay (popover, dialog, or
        // context menu) is open on top of the history view — the history canvas
        // handles mouse-move at the window level, so it still fires under the
        // overlay. If the open overlay is the hover's own item menu, leave the
        // existing hover in place.
        if self.is_overlay_open(cx) && !self.history_refs_hover_host.read(cx).is_item_menu_open() {
            self.close_history_refs_hover(cx);
            return;
        }
        self.history_refs_hover_host.update(cx, |host, cx| {
            host.show(
                repo_id,
                commit_id,
                source_bounds,
                items,
                pointer,
                window,
                cx,
            )
        });
    }

    pub(in crate::view) fn show_commit_message_hover(
        &mut self,
        next: CommitMessageHoverState,
        pointer: Point<Pixels>,
        cx: &mut gpui::Context<Self>,
    ) {
        // Same reasoning as the refs hover: the history canvas listens for
        // mouse-move at the window level, so it still fires under an open
        // overlay and the card would surface on top of it.
        if self.is_overlay_open(cx) {
            self.dismiss_commit_message_hover(cx);
            return;
        }
        self.commit_message_hover_host
            .update(cx, |host, cx| host.show(next, pointer, cx));
    }

    pub(in crate::view) fn dismiss_commit_message_hover(&mut self, cx: &mut gpui::Context<Self>) {
        self.commit_message_hover_host
            .update(cx, |host, cx| host.dismiss(cx));
    }

    pub(in crate::view) fn close_history_refs_hover(&mut self, cx: &mut gpui::Context<Self>) {
        self.history_refs_hover_host
            .update(cx, |host, cx| host.close(cx));
    }

    pub(in crate::view) fn dismiss_history_refs_menus(&mut self, cx: &mut gpui::Context<Self>) {
        self.close_history_refs_hover(cx);

        let history_refs_menu_open =
            self.active_context_menu_invoker
                .as_ref()
                .is_some_and(|invoker| {
                    invoker
                        .as_ref()
                        .starts_with(HISTORY_REFS_HOVER_MENU_INVOKER_PREFIX)
                });
        if history_refs_menu_open {
            self.popover_host
                .update(cx, |host, cx| host.close_popover(cx));
        }
    }

    pub(in crate::view) fn set_history_refs_hover_item_menu_open(
        &mut self,
        open: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_refs_hover_host
            .update(cx, |host, cx| host.set_item_menu_open(open, cx));
    }

    pub(in crate::view) fn set_active_context_menu_invoker(
        &mut self,
        next: Option<SharedString>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.active_context_menu_invoker == next {
            return;
        }
        self.active_context_menu_invoker = next.clone();

        let sidebar_pane = self.sidebar_pane.clone();
        let main_pane = self.main_pane.clone();
        let details_pane = self.details_pane.clone();
        let repo_tabs_bar = self.repo_tabs_bar.clone();
        let action_bar = self.action_bar.clone();
        let bottom_status_bar = self.bottom_status_bar.clone();

        cx.defer(move |cx| {
            sidebar_pane.update(cx, |pane, cx| {
                pane.set_active_context_menu_invoker(next.clone(), cx);
            });
            main_pane.update(cx, |pane, cx| {
                pane.set_active_context_menu_invoker(next.clone(), cx);
            });
            details_pane.update(cx, |pane, cx| {
                pane.set_active_context_menu_invoker(next.clone(), cx);
            });
            repo_tabs_bar.update(cx, |bar, cx| {
                bar.set_active_context_menu_invoker(next.clone(), cx);
            });
            action_bar.update(cx, |bar, cx| {
                bar.set_active_context_menu_invoker(next.clone(), cx);
            });
            bottom_status_bar.update(cx, |bar, cx| {
                bar.set_active_context_menu_invoker(next.clone(), cx);
            });
        });
    }

    pub(in crate::view) fn register_pending_worktree_branch_removal(
        &mut self,
        repo_id: RepoId,
        path: std::path::PathBuf,
        branch: String,
    ) {
        self.pending_worktree_branch_removals
            .insert((repo_id, path), branch);
    }

    fn take_pending_worktree_branch_removal(
        &mut self,
        repo_id: RepoId,
        path: &std::path::Path,
    ) -> Option<String> {
        self.pending_worktree_branch_removals
            .remove(&(repo_id, path.to_path_buf()))
    }

    #[cfg(test)]
    pub fn new(
        store: AppStore,
        events: smol::channel::Receiver<StoreEvent>,
        initial_path: Option<std::path::PathBuf>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) -> Self {
        let config = match initial_path {
            Some(path) => GitCometViewConfig::normal_with_initial_repository(path, None),
            None => GitCometViewConfig::normal(None),
        };
        Self::new_with_config(store, events, config, window, cx)
    }

    pub fn new_with_config(
        store: AppStore,
        events: smol::channel::Receiver<StoreEvent>,
        config: GitCometViewConfig,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) -> Self {
        let GitCometViewConfig {
            mut initial_path,
            initial_repository_launch_mode,
            view_mode,
            focused_mergetool,
            focused_mergetool_exit_code,
            startup_crash_report,
        } = config;
        if initial_path.is_none() {
            initial_path = focused_mergetool.as_ref().map(|cfg| cfg.repo_path.clone());
        }
        let focused_mergetool_labels = focused_mergetool.as_ref().map(|cfg| cfg.labels.clone());
        let focused_mergetool_bootstrap = if view_mode == GitCometViewMode::FocusedMergetool {
            focused_mergetool
                .clone()
                .map(FocusedMergetoolBootstrap::from_view_config)
        } else {
            None
        };
        let store = Arc::new(store);

        let mut ui_session = session::load();
        let ui_scale = ui_scale::current_or_initialize_from_session(&ui_session, cx);
        let _font_preferences =
            crate::font_preferences::current_or_initialize_from_session(window, &ui_session, cx);
        if should_seed_initial_repository_from_session(
            view_mode,
            initial_path.as_deref(),
            initial_repository_launch_mode,
            !ui_session.open_repos.is_empty(),
        ) && let Some(path) = initial_path.as_ref()
        {
            if !ui_session.open_repos.iter().any(|p| p == path) {
                ui_session.open_repos.push(path.clone());
            }
            ui_session.active_repo = Some(path.clone());
        }

        let restored_sidebar_width = ui_session.sidebar_width;
        let restored_details_width = ui_session.details_width;
        let restored_sidebar_collapsed = ui_session.sidebar_collapsed.unwrap_or(false);
        let _ = crate::theme::ensure_user_themes_dir_exists();
        let theme_mode = ui_session
            .theme_mode
            .as_deref()
            .and_then(ThemeMode::from_key)
            .unwrap_or_default();
        let initial_theme = theme_mode.resolve_theme(window.appearance());
        let date_time_format = ui_session
            .date_time_format
            .as_deref()
            .and_then(DateTimeFormat::from_key)
            .unwrap_or(DateTimeFormat::YmdHm);
        let timezone = ui_session
            .timezone
            .as_deref()
            .and_then(Timezone::from_key)
            .unwrap_or_default();
        let show_timezone = ui_session.show_timezone.unwrap_or(true);
        let change_tracking_view = ui_session
            .change_tracking_view
            .as_deref()
            .and_then(ChangeTrackingView::from_key)
            .unwrap_or_default();
        let terminal_preferences = TerminalPreferences::from_ui_session(&ui_session);
        let diff_scroll_sync = ui_session
            .diff_scroll_sync
            .as_deref()
            .and_then(DiffScrollSync::from_key)
            .unwrap_or_default();
        let diff_content_mode = ui_session
            .diff_content_mode
            .as_deref()
            .and_then(DiffContentMode::from_key)
            .unwrap_or_default();
        let diff_whitespace_mode = ui_session
            .diff_whitespace_mode
            .as_deref()
            .and_then(DiffWhitespaceMode::from_key)
            .unwrap_or_default();
        let diff_view_mode = ui_session
            .diff_view_mode
            .as_deref()
            .and_then(DiffViewMode::from_key)
            .unwrap_or(DiffViewMode::Split);
        let annotate_enabled = ui_session.annotate_enabled.unwrap_or(false);
        let diff_reveal_whitespace_chars = ui_session.diff_reveal_whitespace_chars.unwrap_or(false);
        let diff_word_wrap = ui_session.diff_word_wrap.unwrap_or(false);
        let diff_show_line_numbers = ui_session.diff_show_line_numbers.unwrap_or(true);
        let auto_save_file_edits = ui_session.auto_save_file_edits.unwrap_or(false);
        let commit_push_after_enabled = ui_session.commit_push_after_enabled.unwrap_or(false);
        let restored_change_tracking_height = ui_session.change_tracking_height;
        let restored_untracked_height = ui_session.untracked_height;

        let history_show_graph = ui_session.history_show_graph.unwrap_or(true);
        let history_show_author = ui_session.history_show_author.unwrap_or(true);
        let history_show_date = ui_session.history_show_date.unwrap_or(true);
        let history_show_sha = ui_session.history_show_sha.unwrap_or(false);
        let history_relative_dates = ui_session.history_relative_dates.unwrap_or(true);
        let history_highlight_commit_chain =
            ui_session.history_highlight_commit_chain.unwrap_or(true);
        let history_show_tags = ui_session.history_show_tags.unwrap_or(true);
        let history_tag_fetch_mode = ui_session.history_tag_fetch_mode.unwrap_or_default();
        let default_tag_type = ui_session.default_tag_type.unwrap_or_default();
        store.dispatch(Msg::SetGitLogSettings {
            show_history_tags: history_show_tags,
            tag_fetch_mode: history_tag_fetch_mode,
        });
        store.dispatch(Msg::SetDefaultTagType(default_tag_type));
        let saved_open_repos = ui_session.open_repos.clone();
        let saved_active_repo = ui_session.active_repo.clone();
        let mut startup_repo_bootstrap_pending = false;
        let mut deferred_repo_bootstrap = None;

        // Only auto-restore/open on startup if the store hasn't already been preloaded.
        // This avoids re-opening repos (and changing RepoIds) when the UI is attached to an
        // already-initialized store (notably in `gpui::test` setup).
        let initial_store_state = store.snapshot();
        let store_preloaded = !initial_store_state.repos.is_empty();
        let git_runtime_available = initial_store_state.git_runtime.is_available();
        let should_auto_restore = !crate::startup_probe::disable_auto_restore()
            && view_mode != GitCometViewMode::FocusedMergetool
            && crate::ui_runtime::current().auto_restores_session()
            && !store_preloaded;

        if should_auto_restore {
            if !saved_open_repos.is_empty() {
                if git_runtime_available {
                    store.dispatch(Msg::RestoreSession {
                        open_repos: saved_open_repos,
                        active_repo: saved_active_repo,
                    });
                    startup_repo_bootstrap_pending = true;
                } else {
                    deferred_repo_bootstrap = Some(DeferredRepoBootstrap::RestoreSession {
                        open_repos: saved_open_repos,
                        active_repo: saved_active_repo,
                    });
                }
            }
        } else if store_preloaded {
            if let Some(path) = initial_path.as_ref() {
                if git_runtime_available {
                    store.dispatch(Msg::OpenRepo(path.clone()));
                } else {
                    deferred_repo_bootstrap = Some(DeferredRepoBootstrap::OpenRepo(path.clone()));
                }
            }
        } else if let Some(path) = initial_path.as_ref() {
            if git_runtime_available {
                store.dispatch(Msg::OpenRepo(path.clone()));
                startup_repo_bootstrap_pending = true;
            } else {
                deferred_repo_bootstrap = Some(DeferredRepoBootstrap::OpenRepo(path.clone()));
            }
        }

        let initial_state = store.snapshot();
        if !initial_state.repos.is_empty() {
            startup_repo_bootstrap_pending = false;
        }
        let ui_model = cx.new(|_cx| AppUiModel::new(Arc::clone(&initial_state)));

        let ui_model_subscription = cx.observe(&ui_model, |this, model, cx| {
            let next = Arc::clone(&model.read(cx).state);
            let should_quit = crate::startup_probe::observe_app_state(next.as_ref());
            let should_notify = this.apply_state_snapshot(next, cx);
            if should_notify {
                cx.notify();
            }
            if should_quit {
                crate::app::mark_clean_shutdown_from_view(cx);
                cx.quit();
            }
        });

        let weak_view = cx.weak_entity();
        let poller = Poller::start(Arc::clone(&store), events, ui_model.downgrade(), window, cx);

        let title_bar = cx.new(|cx| {
            TitleBarView::new(
                initial_theme,
                weak_view.clone(),
                titlebar_workspace_actions_enabled(view_mode, !initial_state.repos.is_empty()),
                cx,
            )
        });
        let tooltip_host = cx.new(|_cx| TooltipHost::new(initial_theme));
        let toast_host = cx.new(|_cx| ToastHost::new(initial_theme, weak_view.clone()));
        let history_refs_hover_host =
            cx.new(|_cx| HistoryRefsHoverHost::new(initial_theme, weak_view.clone()));
        let commit_message_hover_host = cx.new(|_cx| {
            CommitMessageHoverHost::new(initial_theme, Arc::clone(&store), ui_model.clone())
        });
        let repo_tabs_bar = cx.new(|cx| {
            RepoTabsBarView::new(
                Arc::clone(&store),
                ui_model.clone(),
                initial_theme,
                weak_view.clone(),
                cx,
            )
        });
        let action_bar = cx.new(|cx| {
            ActionBarView::new(
                Arc::clone(&store),
                ui_model.clone(),
                initial_theme,
                weak_view.clone(),
                cx,
            )
        });
        let bottom_status_bar =
            cx.new(|_cx| BottomStatusBarView::new(initial_theme, weak_view.clone()));

        let sidebar_pane = cx.new(|cx| {
            SidebarPaneView::new(
                Arc::clone(&store),
                ui_model.clone(),
                initial_theme,
                ui_session.repo_sidebar_collapsed_items.clone(),
                ui_session.repo_sidebar_pinned_branches.clone(),
                weak_view.clone(),
                tooltip_host.downgrade(),
                cx,
            )
        });
        let main_pane = cx.new(|cx| {
            MainPaneView::new(
                Arc::clone(&store),
                ui_model.clone(),
                initial_theme,
                date_time_format,
                timezone,
                show_timezone,
                history_relative_dates,
                history_highlight_commit_chain,
                diff_scroll_sync,
                diff_content_mode,
                diff_whitespace_mode,
                diff_view_mode,
                annotate_enabled,
                diff_reveal_whitespace_chars,
                diff_word_wrap,
                diff_show_line_numbers,
                auto_save_file_edits,
                history_show_graph,
                history_show_author,
                history_show_date,
                history_show_sha,
                history_show_tags,
                matches!(
                    history_tag_fetch_mode,
                    gitcomet_state::model::GitLogTagFetchMode::OnRepositoryActivation
                ),
                view_mode,
                focused_mergetool_labels,
                focused_mergetool_exit_code.clone(),
                weak_view.clone(),
                tooltip_host.downgrade(),
                window,
                cx,
            )
        });
        main_pane.update(cx, |pane, _cx| {
            pane.mergetool_auto_advance = ui_session.mergetool_auto_advance.unwrap_or(true);
            pane.mergetool_collapse_unchanged =
                ui_session.mergetool_collapse_unchanged.unwrap_or(false);
            pane.mergetool_output_scroll_sync =
                ui_session.mergetool_output_scroll_sync.unwrap_or(true);
            pane.mergetool_show_line_numbers =
                ui_session.mergetool_show_line_numbers.unwrap_or(true);
            pane.mergetool_view_three_way = ui_session.mergetool_view_three_way.unwrap_or(true);
        });
        let details_pane = cx.new(|cx| {
            DetailsPaneView::new(
                Arc::clone(&store),
                ui_model.clone(),
                DetailsPaneInit {
                    theme: initial_theme,
                    change_tracking_view,
                    change_tracking_height: restored_change_tracking_height,
                    untracked_height: restored_untracked_height,
                    ui_scale_percent: ui_scale.percent,
                    commit_push_after_enabled,
                    date_time_format,
                    timezone,
                    show_timezone,
                    root_view: weak_view.clone(),
                    tooltip_host: tooltip_host.downgrade(),
                },
                window,
                cx,
            )
        });

        let reflog_pane = cx.new(|cx| {
            ReflogPaneView::new(
                Arc::clone(&store),
                ui_model.clone(),
                ReflogPaneInit {
                    theme: initial_theme,
                    ui_scale_percent: ui_scale.percent,
                    date_time_format,
                    timezone,
                    show_timezone,
                    root_view: weak_view.clone(),
                },
                cx,
            )
        });

        let popover_host = cx.new(|cx| {
            PopoverHost::new(
                Arc::clone(&store),
                ui_model.clone(),
                initial_theme,
                theme_mode.clone(),
                date_time_format,
                timezone,
                show_timezone,
                change_tracking_view,
                commit_push_after_enabled,
                diff_content_mode,
                diff_whitespace_mode,
                diff_reveal_whitespace_chars,
                diff_word_wrap,
                diff_show_line_numbers,
                weak_view.clone(),
                view_mode,
                tooltip_host.downgrade(),
                main_pane.clone(),
                details_pane.clone(),
                reflog_pane.clone(),
                sidebar_pane.clone(),
                ui_session.repo_sidebar_pinned_branches.clone(),
                ui_session.repo_sidebar_collapsed_items.clone(),
                window,
                cx,
            )
        });

        let command_palette = cx.new(|cx| {
            command_palette::CommandPaletteView::new(
                initial_theme,
                initial_state.active_repo.is_some(),
                weak_view.clone(),
                window,
                cx,
            )
        });

        let activation_subscription = cx.observe_window_activation(window, |this, window, cx| {
            let now = Instant::now();
            if !window.is_window_active() {
                // Leaving the app is one of the two moments auto-save has to
                // mean more than "after a pause": the pending timer would
                // otherwise fire against a window the user has already left,
                // and an external edit to the same file could land first.
                this.main_pane
                    .update(cx, |pane, cx| pane.flush_file_editor_buffer(cx));
                // Capture the focused element before the platform blur() fires and clears it.
                // This is the restore target when opening the palette via a global hotkey while
                // this window is in the background.
                this.pre_palette_focus = window.focused(cx);
                // A deactivation right after we asked for a move/resize grab is
                // the compositor taking focus for the drag, not the user leaving
                // the app. Remember it so the matching re-activation does not
                // refresh the repo.
                this.window_grab_activation_suppressed_at =
                    crate::app::take_window_grab_started_within(now, WINDOW_GRAB_DEACTIVATE_GRACE)
                        .then_some(now);
                return;
            }
            let self_initiated_grab =
                consume_window_grab_activation(&mut this.window_grab_activation_suppressed_at, now);
            let runtime = refresh_git_runtime();
            if runtime != this.state.git_runtime {
                this.store
                    .dispatch(Msg::SetGitRuntimeState(runtime.clone()));
            }
            // Suppressed activations skip `repo_activation_msg` entirely, so its
            // throttle map is not stamped and a genuine alt-tab immediately after
            // a drag still refreshes.
            if !runtime.is_available() || self_initiated_grab {
                return;
            }
            if let Some(msg) =
                repo_activation_msg(&this.state, &mut this.last_repo_activation_dispatch_at, now)
            {
                // Other worktrees have no watcher of their own — the repo
                // monitor only flushes for the active repo — so coming back to
                // the window is the moment their uncommitted-change counts get
                // reconciled. Rides the same throttle as the activation refresh.
                if let Some(repo_id) = this.state.active_repo {
                    this.store.dispatch(Msg::LoadWorktreeDirty { repo_id });
                }
                this.store.dispatch(msg);
            }
        });

        let appearance_subscription = {
            let view = cx.weak_entity();
            let mut first = true;
            window.observe_window_appearance(move |window, app| {
                if first {
                    first = false;
                    return;
                }
                let _ = view.update(app, |this, cx| {
                    if !this.theme_mode.is_automatic() {
                        return;
                    }
                    let theme = this.theme_mode.resolve_theme(window.appearance());
                    this.set_theme(theme, cx);
                    cx.notify();
                });
            })
        };

        let open_repo_input = cx.new(|cx| {
            components::TextInput::new(
                components::TextInputOptions {
                    placeholder: "/path/to/repo".into(),
                    ..Default::default()
                },
                window,
                cx,
            )
        });

        let error_banner_input = cx.new(|cx| {
            components::TextInput::new(
                components::TextInputOptions {
                    multiline: true,
                    read_only: true,
                    chromeless: true,
                    ..Default::default()
                },
                window,
                cx,
            )
        });

        let auth_prompt_username_input = cx.new(|cx| {
            components::TextInput::new(
                components::TextInputOptions {
                    placeholder: "Username".into(),
                    ..Default::default()
                },
                window,
                cx,
            )
        });

        let auth_prompt_secret_input = cx.new(|cx| {
            let mut input = components::TextInput::new(
                components::TextInputOptions {
                    placeholder: "Password / passphrase / confirmation".into(),
                    ..Default::default()
                },
                window,
                cx,
            );
            input.set_masked(true, cx);
            input
        });

        let open_repo_input_subscription = cx.observe(&open_repo_input, |this, input, cx| {
            let enter_pressed = input.update(cx, |input, _| input.take_enter_pressed());
            let escape_pressed = input.update(cx, |input, _| input.take_escape_pressed());

            if !this.open_repo_panel {
                return;
            }

            if escape_pressed {
                this.open_repo_panel = false;
                cx.notify();
                return;
            }
            if enter_pressed {
                this.submit_open_repo_panel(cx);
                return;
            }
            cx.notify();
        });

        let auth_prompt_username_input_subscription =
            cx.observe(&auth_prompt_username_input, |this, input, cx| {
                let enter_pressed = input.update(cx, |input, _| input.take_enter_pressed());
                let escape_pressed = input.update(cx, |input, _| input.take_escape_pressed());

                if escape_pressed {
                    this.store.dispatch(Msg::CancelAuthPrompt);
                    cx.notify();
                    return;
                }
                if enter_pressed {
                    this.try_auth_prompt_submit(cx);
                    return;
                }
                cx.notify();
            });

        let auth_prompt_secret_input_subscription =
            cx.observe(&auth_prompt_secret_input, |this, input, cx| {
                let enter_pressed = input.update(cx, |input, _| input.take_enter_pressed());
                let escape_pressed = input.update(cx, |input, _| input.take_escape_pressed());

                if escape_pressed {
                    this.store.dispatch(Msg::CancelAuthPrompt);
                    cx.notify();
                    return;
                }
                if enter_pressed {
                    this.try_auth_prompt_submit(cx);
                    return;
                }
                cx.notify();
            });

        let scale = ui_scale::UiScale::from_percent(ui_scale.percent);
        let initial_sidebar_width_design =
            ui_scale::design_units_from_stored(restored_sidebar_width)
                .unwrap_or(280.0)
                .max(SIDEBAR_MIN_PX);
        let initial_details_width_design =
            ui_scale::design_units_from_stored(restored_details_width)
                .unwrap_or(420.0)
                .max(DETAILS_MIN_PX);
        let initial_sidebar_width = scale.px(initial_sidebar_width_design);
        let initial_details_width = scale.px(initial_details_width_design);
        // Reopen collapsed if the user quit while collapsed: the render width must
        // also start at the collapsed strip so it doesn't flash open on launch.
        let initial_sidebar_render_width = if restored_sidebar_collapsed {
            scale.px(PANE_COLLAPSED_PX)
        } else {
            initial_sidebar_width
        };

        let terminal_keystroke_interceptor = Self::install_terminal_keystroke_interceptor(cx);

        let mut view = Self {
            state: Arc::clone(&initial_state),
            window_handle: window.window_handle(),
            _ui_model: ui_model,
            store,
            _poller: poller,
            _ui_model_subscription: ui_model_subscription,
            _activation_subscription: activation_subscription,
            _appearance_subscription: appearance_subscription,
            _terminal_keystroke_interceptor: terminal_keystroke_interceptor,
            _auth_prompt_username_input_subscription: auth_prompt_username_input_subscription,
            _open_repo_input_subscription: open_repo_input_subscription,
            _auth_prompt_secret_input_subscription: auth_prompt_secret_input_subscription,
            view_mode,
            theme_mode,
            theme: initial_theme,
            title_bar,
            sidebar_pane,
            main_pane,
            details_pane,
            repo_tabs_bar,
            action_bar,
            bottom_status_bar,
            tooltip_host,
            toast_host,
            history_refs_hover_host,
            commit_message_hover_host,
            popover_host,
            command_palette,
            command_palette_open: false,
            pre_palette_focus: None,
            focused_mergetool_bootstrap,
            submodule_diff_bootstrap: None,
            deferred_repo_bootstrap,
            startup_repo_bootstrap_pending,
            splash_backdrop_image: splash::load_splash_backdrop_image(),
            last_window_size: size(px(0.0), px(0.0)),
            ui_window_size_last_seen: size(px(0.0), px(0.0)),
            ui_settings_persist_seq: 0,
            last_repo_activation_dispatch_at: FxHashMap::default(),
            window_grab_activation_suppressed_at: None,
            date_time_format,
            timezone,
            show_timezone,
            change_tracking_view,
            terminal_preferences,
            terminal_sessions: FxHashMap::default(),
            terminal_panel_height: px(TERMINAL_PANEL_DEFAULT_HEIGHT_PX),
            terminal_panel_resize: None,
            next_terminal_session_seq: 1,
            terminal_cursor_blink_visible: true,
            terminal_cursor_blink_hold_until: Instant::now(),
            terminal_cursor_blink_active: false,
            terminal_cursor_blink_task_scheduled: false,
            terminal_cursor_blink_seq: 0,
            reflog_pane,
            active_bottom_panel: FxHashMap::default(),
            commit_push_after_enabled,
            diff_scroll_sync,
            diff_content_mode,
            diff_whitespace_mode,
            diff_view_mode,
            annotate_enabled,
            diff_reveal_whitespace_chars,
            diff_word_wrap,
            diff_show_line_numbers,
            auto_save_file_edits,
            ui_scale_percent: ui_scale.percent,
            open_repo_panel: false,
            open_repo_input,
            hover_resize_edge: None,
            sidebar_collapsed: restored_sidebar_collapsed,
            sidebar_collapsed_popover: None,
            sidebar_collapsed_popover_closing: None,
            sidebar_collapsed_popover_anim_seq: 0,
            sidebar_collapsed_before_merge_view: None,
            details_collapsed: false,
            sidebar_width_design: initial_sidebar_width_design,
            details_width_design: initial_details_width_design,
            sidebar_width: initial_sidebar_width,
            details_width: initial_details_width,
            sidebar_render_width: initial_sidebar_render_width,
            details_render_width: initial_details_width,
            sidebar_width_anim_seq: 0,
            details_width_anim_seq: 0,
            sidebar_width_animating: false,
            details_width_animating: false,
            pane_resize: None,
            last_mouse_pos: point(px(0.0), px(0.0)),
            pending_terminal_shutdown_prompt: None,
            pending_unsaved_file_edits_prompt: None,
            pending_unsaved_file_edits_flush: None,
            pending_quit_other_views: Vec::new(),
            pending_pull_reconcile_prompt: None,
            pending_force_delete_branch_prompt: None,
            pending_force_delete_branch_centered: false,
            pending_force_remove_worktree_prompt: None,
            pending_submodule_trust_prompt: None,
            pending_submodule_trust_check: None,
            pending_worktree_branch_removals: FxHashMap::default(),
            startup_crash_report,
            #[cfg(target_os = "macos")]
            recent_repos_menu_fingerprint: ui_session.recent_repos.clone(),
            error_banner_input,
            auth_prompt_username_input,
            auth_prompt_secret_input,
            auth_prompt_key: None,
            active_context_menu_invoker: None,
        };

        view.set_theme(initial_theme, cx);
        view.sync_action_bar_terminal_target(cx);

        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
        view.maybe_auto_install_linux_desktop_integration(cx);

        view.drive_focused_mergetool_bootstrap();
        view.drive_submodule_diff_bootstrap();
        view.maybe_show_user_survey_on_startup(cx);
        view.maybe_check_for_updates_on_startup(cx);

        crate::app::sync_gitcomet_window_state(
            cx,
            view.window_handle,
            cx.weak_entity(),
            view.main_pane.downgrade(),
            view.view_mode,
            view.state
                .repos
                .iter()
                .map(|repo| repo.spec.workdir.clone())
                .collect(),
        );

        view
    }

    fn set_theme(&mut self, theme: AppTheme, cx: &mut gpui::Context<Self>) {
        self.theme = theme;
        for session in self.terminal_sessions.values() {
            for instance in &session.instances {
                instance.viewport.update(cx, |viewport, cx| {
                    viewport.set_theme(theme, cx);
                });
            }
        }
        self.title_bar
            .update(cx, |bar, cx| bar.set_theme(theme, cx));
        self.sidebar_pane
            .update(cx, |pane, cx| pane.set_theme(theme, cx));
        self.main_pane
            .update(cx, |pane, cx| pane.set_theme(theme, cx));
        self.details_pane
            .update(cx, |pane, cx| pane.set_theme(theme, cx));
        self.reflog_pane
            .update(cx, |pane, cx| pane.set_theme(theme, cx));
        self.repo_tabs_bar
            .update(cx, |bar, cx| bar.set_theme(theme, cx));
        self.action_bar
            .update(cx, |bar, cx| bar.set_theme(theme, cx));
        self.bottom_status_bar
            .update(cx, |bar, cx| bar.set_theme(theme, cx));
        self.tooltip_host
            .update(cx, |host, cx| host.set_theme(theme, cx));
        self.toast_host
            .update(cx, |host, cx| host.set_theme(theme, cx));
        self.history_refs_hover_host
            .update(cx, |host, cx| host.set_theme(theme, cx));
        self.commit_message_hover_host
            .update(cx, |host, cx| host.set_theme(theme, cx));
        self.popover_host
            .update(cx, |host, cx| host.set_theme(theme, cx));
        self.command_palette
            .update(cx, |palette, cx| palette.set_theme(theme, cx));
        self.open_repo_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.error_banner_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.auth_prompt_username_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.auth_prompt_secret_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        cx.notify();
    }

    fn notify_font_preferences_changed(&mut self, cx: &mut gpui::Context<Self>) {
        for session in self.terminal_sessions.values() {
            for instance in &session.instances {
                instance.viewport.update(cx, |viewport, cx| {
                    viewport.invalidate_layout(cx);
                });
            }
        }
        self.title_bar.update(cx, |_bar, cx| cx.notify());
        self.sidebar_pane.update(cx, |_pane, cx| cx.notify());
        self.main_pane
            .update(cx, |pane, cx| pane.invalidate_font_metrics(cx));
        self.details_pane.update(cx, |_pane, cx| cx.notify());
        self.reflog_pane.update(cx, |_pane, cx| cx.notify());
        self.repo_tabs_bar.update(cx, |_bar, cx| cx.notify());
        self.action_bar.update(cx, |_bar, cx| cx.notify());
        self.bottom_status_bar.update(cx, |_bar, cx| cx.notify());
        self.tooltip_host.update(cx, |_host, cx| cx.notify());
        self.toast_host.update(cx, |_host, cx| cx.notify());
        self.popover_host.update(cx, |_host, cx| cx.notify());
        self.open_repo_input.update(cx, |_input, cx| cx.notify());
        self.error_banner_input.update(cx, |_input, cx| cx.notify());
        self.auth_prompt_username_input
            .update(cx, |_input, cx| cx.notify());
        self.auth_prompt_secret_input
            .update(cx, |_input, cx| cx.notify());
        cx.notify();
    }

    /// Repaint the panes that show which files have unsaved editor buffers.
    ///
    /// The main pane owns those buffers and the sidebar draws them, and the two
    /// are separate entities with no store snapshot between them to carry the
    /// change — so the pane that changed it says so, here.
    pub(in crate::view) fn notify_unsaved_file_edits_changed(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        self.sidebar_pane.update(cx, |_pane, cx| cx.notify());
        cx.notify();
    }

    fn ui_scale(&self) -> ui_scale::UiScale {
        ui_scale::UiScale::from_percent(self.ui_scale_percent)
    }

    fn sync_cached_pane_widths_from_design(&mut self) {
        let scale = self.ui_scale();
        self.sidebar_width = scale.px(self.sidebar_width_design);
        self.details_width = scale.px(self.details_width_design);
    }

    fn set_sidebar_width_from_pixels(&mut self, width: Pixels) {
        self.sidebar_width = width;
        self.sidebar_width_design = self.ui_scale().design_units_from_pixels(width);
    }

    fn set_details_width_from_pixels(&mut self, width: Pixels) {
        self.details_width = width;
        self.details_width_design = self.ui_scale().design_units_from_pixels(width);
    }

    fn scaled_px(&self, value: f32) -> Pixels {
        self.ui_scale().px(value)
    }

    fn pane_collapsed_width(&self) -> Pixels {
        self.scaled_px(PANE_COLLAPSED_PX)
    }

    fn main_min_width(&self) -> Pixels {
        self.scaled_px(MAIN_MIN_PX)
    }

    fn sidebar_min_width(&self) -> Pixels {
        self.scaled_px(SIDEBAR_MIN_PX)
    }

    fn details_min_width(&self) -> Pixels {
        self.scaled_px(DETAILS_MIN_PX)
    }

    fn pane_resize_handle_width(&self) -> Pixels {
        self.scaled_px(PANE_RESIZE_HANDLE_PX)
    }

    pub(crate) fn apply_ui_scale_percent(
        &mut self,
        percent: u32,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        let percent = ui_scale::sanitize_percent(Some(percent));
        if self.ui_scale_percent == percent {
            return;
        }

        let previous_percent = self.ui_scale_percent;
        let scale = self.ui_scale();
        self.sidebar_width_design = scale.design_units_from_pixels(self.sidebar_width);
        self.details_width_design = scale.design_units_from_pixels(self.details_width);
        self.ui_scale_percent = percent;
        self.pane_resize = None;
        self.sidebar_width_anim_seq = self.sidebar_width_anim_seq.wrapping_add(1);
        self.details_width_anim_seq = self.details_width_anim_seq.wrapping_add(1);
        self.sidebar_width_animating = false;
        self.details_width_animating = false;

        ui_scale::apply_to_window(window, percent);
        crate::app::ensure_window_respects_min_size(
            window,
            crate::app::main_window_min_size_for_percent(percent),
        );

        self.last_window_size = window.viewport_size();
        self.ui_window_size_last_seen = self.last_window_size;
        self.sync_cached_pane_widths_from_design();

        let change_tracking_view = self.change_tracking_view;
        self.details_pane.update(cx, |pane, cx| {
            pane.apply_ui_scale_percent(previous_percent, percent, change_tracking_view, cx);
        });
        self.main_pane.update(cx, |pane, cx| {
            pane.apply_ui_scale_percent(previous_percent, percent, cx);
        });
        self.reflog_pane.update(cx, |pane, cx| {
            pane.set_ui_scale_percent(percent, cx);
        });
        self.popover_host.update(cx, |_host, cx| {
            cx.notify();
        });

        self.clamp_pane_widths_to_window();
        self.notify_font_preferences_changed(cx);
        self.schedule_ui_settings_persist(cx);
    }

    fn set_theme_mode(
        &mut self,
        mode: ThemeMode,
        appearance: gpui::WindowAppearance,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.theme_mode == mode {
            return;
        }

        self.theme_mode = mode.clone();
        self.set_theme(mode.resolve_theme(appearance), cx);
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_change_tracking_view(
        &mut self,
        next: ChangeTrackingView,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.change_tracking_view == next {
            return;
        }

        self.change_tracking_view = next;
        self.details_pane
            .update(cx, |pane, cx| pane.set_change_tracking_view(next, cx));
        self.popover_host
            .update(cx, |host, cx| host.sync_change_tracking_view(next, cx));
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_commit_push_after_enabled(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.commit_push_after_enabled == enabled {
            return;
        }

        self.commit_push_after_enabled = enabled;
        self.details_pane.update(cx, |pane, cx| {
            pane.set_commit_push_after_enabled(enabled, cx)
        });
        self.popover_host.update(cx, |host, cx| {
            host.sync_commit_push_after_enabled(enabled, cx)
        });
        self.schedule_ui_settings_persist(cx);
        cx.notify();
    }

    pub(in crate::view) fn set_commit_amend_enabled(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.details_pane
            .update(cx, |pane, cx| pane.set_commit_amend_enabled(enabled, cx));
        self.popover_host
            .update(cx, |host, cx| host.sync_commit_amend_enabled(enabled, cx));
        cx.notify();
    }

    pub(in crate::view) fn set_diff_scroll_sync(
        &mut self,
        next: DiffScrollSync,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_scroll_sync == next {
            return;
        }

        self.diff_scroll_sync = next;
        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_scroll_sync(next, cx));
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_diff_view_mode(
        &mut self,
        next: DiffViewMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_view_mode == next {
            return;
        }

        self.diff_view_mode = next;
        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_view_mode(next, cx));
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_annotate_enabled(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.annotate_enabled == next {
            return;
        }

        self.annotate_enabled = next;
        // Blame is an annotation column, not a view mode: it renders in the left
        // column in Split (see `rows::diff`, `annotation_active() && is_left`)
        // just as it does in Inline, and the wrap widths already account for it
        // in both. Toggling it must leave the selected mode alone.
        self.main_pane
            .update(cx, |pane, cx| pane.set_annotate_enabled(next, cx));
        self.schedule_ui_settings_persist(cx);
    }

    fn apply_diff_content_mode_preference(
        &mut self,
        next: DiffContentMode,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.diff_content_mode == next {
            return false;
        }

        self.diff_content_mode = next;
        self.popover_host
            .update(cx, |host, cx| host.sync_diff_content_mode(next, cx));
        self.schedule_ui_settings_persist(cx);
        true
    }

    // MainPaneView sometimes owns the active GPUI update when the diff-header
    // toggle is clicked, so syncing the root preference must not call back into
    // `main_pane.update(...)`.
    pub(in crate::view) fn sync_diff_content_mode_from_pane(
        &mut self,
        next: DiffContentMode,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.apply_diff_content_mode_preference(next, cx);
    }

    pub(in crate::view) fn set_diff_content_mode(
        &mut self,
        next: DiffContentMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.apply_diff_content_mode_preference(next, cx) {
            return;
        }

        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_content_mode(next, cx));
    }

    fn apply_diff_whitespace_mode_preference(
        &mut self,
        next: DiffWhitespaceMode,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.diff_whitespace_mode == next {
            return false;
        }

        self.diff_whitespace_mode = next;
        self.popover_host
            .update(cx, |host, cx| host.sync_diff_whitespace_mode(next, cx));
        self.schedule_ui_settings_persist(cx);
        true
    }

    pub(in crate::view) fn sync_diff_whitespace_mode_from_pane(
        &mut self,
        next: DiffWhitespaceMode,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.apply_diff_whitespace_mode_preference(next, cx);
    }

    pub(in crate::view) fn set_diff_whitespace_mode(
        &mut self,
        next: DiffWhitespaceMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.apply_diff_whitespace_mode_preference(next, cx) {
            return;
        }

        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_whitespace_mode(next, cx));
    }

    fn apply_diff_reveal_whitespace_chars_preference(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.diff_reveal_whitespace_chars == next {
            return false;
        }

        self.diff_reveal_whitespace_chars = next;
        self.popover_host.update(cx, |host, cx| {
            host.sync_diff_reveal_whitespace_chars(next, cx)
        });
        self.schedule_ui_settings_persist(cx);
        true
    }

    pub(in crate::view) fn sync_diff_reveal_whitespace_chars_from_pane(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.apply_diff_reveal_whitespace_chars_preference(next, cx);
    }

    pub(in crate::view) fn set_diff_reveal_whitespace_chars(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.apply_diff_reveal_whitespace_chars_preference(next, cx) {
            return;
        }

        self.main_pane.update(cx, |pane, cx| {
            pane.set_diff_reveal_whitespace_chars(next, cx)
        });
    }

    fn apply_diff_word_wrap_preference(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.diff_word_wrap == next {
            return false;
        }

        self.diff_word_wrap = next;
        self.popover_host
            .update(cx, |host, cx| host.sync_diff_word_wrap(next, cx));
        self.schedule_ui_settings_persist(cx);
        true
    }

    pub(in crate::view) fn sync_diff_word_wrap_from_pane(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.apply_diff_word_wrap_preference(next, cx);
    }

    pub(in crate::view) fn set_diff_word_wrap(&mut self, next: bool, cx: &mut gpui::Context<Self>) {
        if !self.apply_diff_word_wrap_preference(next, cx) {
            return;
        }

        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_word_wrap(next, cx));
    }

    fn apply_diff_show_line_numbers_preference(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.diff_show_line_numbers == next {
            return false;
        }

        self.diff_show_line_numbers = next;
        self.popover_host
            .update(cx, |host, cx| host.sync_diff_show_line_numbers(next, cx));
        self.schedule_ui_settings_persist(cx);
        true
    }

    pub(in crate::view) fn sync_diff_show_line_numbers_from_pane(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.apply_diff_show_line_numbers_preference(next, cx);
    }

    pub(in crate::view) fn set_diff_show_line_numbers(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.apply_diff_show_line_numbers_preference(next, cx) {
            return;
        }

        self.main_pane
            .update(cx, |pane, cx| pane.set_diff_show_line_numbers(next, cx));
    }

    /// Show the file the main pane has open in the sidebar's file explorer,
    /// expanding the folders on the way to it and scrolling it into view.
    ///
    /// Switches the sidebar to Files when it is showing Branches — the action is
    /// reachable from the menu, the palette and a shortcut, so the tree it acts
    /// on may not even be visible.
    pub(crate) fn locate_open_file_in_explorer(&mut self, cx: &mut gpui::Context<Self>) {
        if self.sidebar_collapsed {
            self.set_sidebar_collapsed(false, cx);
        }
        self.sidebar_pane
            .update(cx, |pane, cx| pane.locate_open_file(cx));
        cx.notify();
    }

    /// Mirrors the settings window's auto-save toggle into the pane that owns
    /// the file editor. The main window never writes this back (the settings
    /// window is the only writer), so there is no persist call here.
    pub(in crate::view) fn set_auto_save_file_edits(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.auto_save_file_edits == next {
            return;
        }
        self.auto_save_file_edits = next;
        self.main_pane
            .update(cx, |pane, cx| pane.set_auto_save_file_edits(next, cx));
    }

    pub(in crate::view) fn set_history_column_preferences(
        &mut self,
        show_graph: bool,
        show_author: bool,
        show_date: bool,
        show_sha: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.main_pane.update(cx, |pane, cx| {
            pane.set_history_column_preferences(show_graph, show_author, show_date, show_sha, cx);
        });
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn reset_history_column_widths(&mut self, cx: &mut gpui::Context<Self>) {
        self.main_pane
            .update(cx, |pane, cx| pane.reset_history_column_widths(cx));
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_history_highlight_commit_chain(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.main_pane.update(cx, |pane, cx| {
            pane.set_history_highlight_commit_chain(enabled, cx);
        });
        cx.notify();
    }

    pub(in crate::view) fn set_history_relative_dates(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.main_pane.update(cx, |pane, cx| {
            pane.set_history_relative_dates(enabled, cx);
        });
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_history_tag_preferences(
        &mut self,
        show_tags: bool,
        tag_fetch_mode: gitcomet_state::model::GitLogTagFetchMode,
        cx: &mut gpui::Context<Self>,
    ) {
        let auto_fetch_tags_on_repo_activation = matches!(
            tag_fetch_mode,
            gitcomet_state::model::GitLogTagFetchMode::OnRepositoryActivation
        );
        self.main_pane.update(cx, |pane, cx| {
            pane.set_history_tag_preferences(show_tags, auto_fetch_tags_on_repo_activation, cx);
        });
        self.store.dispatch(Msg::SetGitLogSettings {
            show_history_tags: show_tags,
            tag_fetch_mode,
        });
        if show_tags
            && auto_fetch_tags_on_repo_activation
            && let Some(repo) = self.main_pane.read(cx).active_repo()
        {
            if matches!(repo.tags, Loadable::NotLoaded | Loadable::Error(_)) {
                self.store.dispatch(Msg::LoadTags { repo_id: repo.id });
            }
            if matches!(repo.remote_tags, Loadable::NotLoaded | Loadable::Error(_)) {
                self.store
                    .dispatch(Msg::LoadRemoteTags { repo_id: repo.id });
            }
        }
        self.schedule_ui_settings_persist(cx);
    }

    pub(in crate::view) fn set_default_tag_type_preference(
        &mut self,
        tag_type: DefaultTagType,
        _cx: &mut gpui::Context<Self>,
    ) {
        self.store.dispatch(Msg::SetDefaultTagType(tag_type));
    }

    fn refresh_main_pane_after_panel_animation(&mut self, cx: &mut gpui::Context<Self>) {
        let main_pane = self.main_pane.clone();
        cx.defer(move |cx| {
            main_pane.update(cx, |pane, cx| {
                pane.sync_root_layout_snapshot(cx);
                cx.notify();
            });
        });
    }

    /// Evaluate a CSS-style `cubic-bezier(x1, y1, x2, y2)` timing function at
    /// progress `t` in `[0, 1]`. Endpoints P0=(0,0) and P3=(1,1) are implicit.
    ///
    /// The curve is parametric in `s`, so for a given time `t` we first solve
    /// `bezier_x(s) = t` (a few Newton-Raphson steps — the x-curve is monotonic
    /// for the control points we use) and then read off `bezier_y(s)`.
    fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> f32 {
        if t <= 0.0 {
            return 0.0;
        }
        if t >= 1.0 {
            return 1.0;
        }

        // B(s) = 3(1-s)^2 s c1 + 3(1-s) s^2 c2 + s^3, with c0 = 0, c3 = 1.
        let bezier = |c1: f32, c2: f32, s: f32| {
            let inv = 1.0 - s;
            3.0 * inv * inv * s * c1 + 3.0 * inv * s * s * c2 + s * s * s
        };
        // B'(s) = 3(1-s)^2 c1 + 6(1-s) s (c2 - c1) + 3 s^2 (1 - c2).
        let bezier_prime = |c1: f32, c2: f32, s: f32| {
            let inv = 1.0 - s;
            3.0 * inv * inv * c1 + 6.0 * inv * s * (c2 - c1) + 3.0 * s * s * (1.0 - c2)
        };

        let mut s = t;
        for _ in 0..8 {
            let x = bezier(x1, x2, s) - t;
            if x.abs() < 1e-4 {
                break;
            }
            let dx = bezier_prime(x1, x2, s);
            if dx.abs() < 1e-6 {
                break;
            }
            s = (s - x / dx).clamp(0.0, 1.0);
        }

        bezier(y1, y2, s)
    }

    /// Easing for pane collapse/expand: a "fast-out, slow-in" cubic bezier
    /// (the Material standard curve) that reads smoothly in both directions.
    fn pane_collapse_ease(t: f32) -> f32 {
        Self::cubic_bezier(0.4, 0.0, 0.2, 1.0, t)
    }

    fn animate_sidebar_render_width_to(&mut self, target: Pixels, cx: &mut gpui::Context<Self>) {
        let start = self.sidebar_render_width;
        let start_f: f32 = start.into();
        let target_f: f32 = target.into();
        self.sidebar_width_anim_seq = self.sidebar_width_anim_seq.wrapping_add(1);
        let seq = self.sidebar_width_anim_seq;
        if (start_f - target_f).abs() <= 0.5 {
            self.sidebar_render_width = target;
            self.sidebar_width_animating = false;
            return;
        }

        if !crate::ui_runtime::current().uses_pane_animations() {
            self.sidebar_render_width = target;
            self.sidebar_width_animating = false;
            self.refresh_main_pane_after_panel_animation(cx);
            cx.notify();
            return;
        }

        self.sidebar_width_animating = true;
        let started = Instant::now();
        let duration = Duration::from_millis(PANE_COLLAPSE_ANIM_MS);

        cx.spawn(
            async move |view: WeakEntity<GitCometView>, cx: &mut gpui::AsyncApp| loop {
                smol::Timer::after(Duration::from_millis(16)).await;

                let mut t =
                    started.elapsed().as_secs_f32() / duration.as_secs_f32().max(f32::EPSILON);
                if !t.is_finite() {
                    t = 1.0;
                }
                let t = t.clamp(0.0, 1.0);
                let eased = Self::pane_collapse_ease(t);
                let mut done = t >= 1.0;

                let _ = view.update(cx, |this, cx| {
                    if this.sidebar_width_anim_seq != seq {
                        done = true;
                        return;
                    }

                    let mut changed = false;
                    let next_width = px(start_f + (target_f - start_f) * eased);
                    if this.sidebar_render_width != next_width {
                        this.sidebar_render_width = next_width;
                        changed = true;
                    }
                    if t >= 1.0 {
                        if this.sidebar_render_width != px(target_f) {
                            this.sidebar_render_width = px(target_f);
                        }
                        this.sidebar_width_animating = false;
                        this.refresh_main_pane_after_panel_animation(cx);
                        changed = true;
                    }
                    if changed {
                        cx.notify();
                    }
                });

                if done {
                    break;
                }
            },
        )
        .detach();
    }

    fn animate_details_render_width_to(&mut self, target: Pixels, cx: &mut gpui::Context<Self>) {
        let start = self.details_render_width;
        let start_f: f32 = start.into();
        let target_f: f32 = target.into();
        self.details_width_anim_seq = self.details_width_anim_seq.wrapping_add(1);
        let seq = self.details_width_anim_seq;
        if (start_f - target_f).abs() <= 0.5 {
            self.details_render_width = target;
            self.details_width_animating = false;
            return;
        }

        if !crate::ui_runtime::current().uses_pane_animations() {
            self.details_render_width = target;
            self.details_width_animating = false;
            self.refresh_main_pane_after_panel_animation(cx);
            cx.notify();
            return;
        }

        self.details_width_animating = true;
        let started = Instant::now();
        let duration = Duration::from_millis(PANE_COLLAPSE_ANIM_MS);

        cx.spawn(
            async move |view: WeakEntity<GitCometView>, cx: &mut gpui::AsyncApp| loop {
                smol::Timer::after(Duration::from_millis(16)).await;

                let mut t =
                    started.elapsed().as_secs_f32() / duration.as_secs_f32().max(f32::EPSILON);
                if !t.is_finite() {
                    t = 1.0;
                }
                let t = t.clamp(0.0, 1.0);
                let eased = Self::pane_collapse_ease(t);
                let mut done = t >= 1.0;

                let _ = view.update(cx, |this, cx| {
                    if this.details_width_anim_seq != seq {
                        done = true;
                        return;
                    }

                    let mut changed = false;
                    let next_width = px(start_f + (target_f - start_f) * eased);
                    if this.details_render_width != next_width {
                        this.details_render_width = next_width;
                        changed = true;
                    }
                    if t >= 1.0 {
                        if this.details_render_width != px(target_f) {
                            this.details_render_width = px(target_f);
                        }
                        this.details_width_animating = false;
                        this.refresh_main_pane_after_panel_animation(cx);
                        changed = true;
                    }
                    if changed {
                        cx.notify();
                    }
                });

                if done {
                    break;
                }
            },
        )
        .detach();
    }

    fn set_sidebar_collapsed(&mut self, collapsed: bool, cx: &mut gpui::Context<Self>) {
        if self.sidebar_collapsed == collapsed {
            return;
        }

        self.sidebar_collapsed = collapsed;
        // The collapsed-rail popover only exists while collapsed; drop it (and any
        // in-flight fade) instantly when the full sidebar comes back so it can't
        // linger over the expanded pane.
        if !collapsed {
            self.sidebar_collapsed_popover = None;
            self.sidebar_collapsed_popover_closing = None;
            self.sidebar_collapsed_popover_anim_seq =
                self.sidebar_collapsed_popover_anim_seq.wrapping_add(1);
        }
        if matches!(
            self.pane_resize,
            Some(PaneResizeState {
                handle: PaneResizeHandle::Sidebar,
                ..
            })
        ) {
            self.pane_resize = None;
        }
        if !collapsed {
            // Mark the sidebar as animating before clamping: the width reconcile
            // in `clamp_pane_widths_to_window` snaps `sidebar_render_width` to the
            // target whenever it isn't animating, which would collapse the open
            // animation to a single frame (start == target). With the flag set it
            // preserves the current (collapsed) render width so the animation below
            // can grow it out.
            self.sidebar_width_animating = true;
            self.clamp_pane_widths_to_window();
        }

        let target = if collapsed {
            self.pane_collapsed_width()
        } else {
            self.sidebar_width
        };
        self.animate_sidebar_render_width_to(target, cx);
        // Persist so the sidebar reopens in the same state next launch.
        self.schedule_ui_settings_persist(cx);
        cx.notify();
    }

    /// Toggle the collapsed-sidebar popover for `section`. Clicking the icon of
    /// the open section closes it; clicking a different one switches to it and
    /// triggers any lazy data load that section needs.
    pub(in crate::view) fn toggle_sidebar_collapsed_popover(
        &mut self,
        section: CollapsedSidebarSection,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.sidebar_collapsed_popover == Some(section) {
            self.close_sidebar_collapsed_popover(cx);
        } else {
            self.open_sidebar_collapsed_popover(section, cx);
        }
    }

    fn open_sidebar_collapsed_popover(
        &mut self,
        section: CollapsedSidebarSection,
        cx: &mut gpui::Context<Self>,
    ) {
        self.sidebar_collapsed_popover = Some(section);
        self.sidebar_collapsed_popover_closing = None;
        self.sidebar_collapsed_popover_anim_seq =
            self.sidebar_collapsed_popover_anim_seq.wrapping_add(1);
        self.sidebar_pane.update(cx, |pane, cx| {
            pane.ensure_collapsed_section_data(section, cx);
        });
        cx.notify();
    }

    /// Begin dismissing the popover: hand the section to `..._closing` so it stays
    /// mounted for the fade-out, then clear it after the fade with a seq-guarded
    /// timer so a fresh open during the fade isn't clobbered.
    pub(in crate::view) fn close_sidebar_collapsed_popover(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(section) = self.sidebar_collapsed_popover.take() else {
            return;
        };
        self.sidebar_collapsed_popover_closing = Some(section);
        self.sidebar_collapsed_popover_anim_seq =
            self.sidebar_collapsed_popover_anim_seq.wrapping_add(1);
        let seq = self.sidebar_collapsed_popover_anim_seq;
        cx.notify();

        // Time the fade-out on the app's executor rather than a bare
        // `smol::Timer`, which would arm the global reactor and fire on its own
        // thread — deterministic under test, identical in the running app.
        let fade_out = cx
            .background_executor()
            .timer(Duration::from_millis(COLLAPSED_POPOVER_FADE_MS));
        cx.spawn(
            async move |view: WeakEntity<GitCometView>, cx: &mut gpui::AsyncApp| {
                fade_out.await;
                let _ = view.update(cx, |this, cx| {
                    if this.sidebar_collapsed_popover_anim_seq == seq
                        && this.sidebar_collapsed_popover_closing.is_some()
                    {
                        this.sidebar_collapsed_popover_closing = None;
                        cx.notify();
                    }
                });
            },
        )
        .detach();
    }

    fn set_details_collapsed(&mut self, collapsed: bool, cx: &mut gpui::Context<Self>) {
        if self.details_collapsed == collapsed {
            return;
        }

        self.details_collapsed = collapsed;
        if matches!(
            self.pane_resize,
            Some(PaneResizeState {
                handle: PaneResizeHandle::Details,
                ..
            })
        ) {
            self.pane_resize = None;
        }
        if !collapsed {
            // Same reasoning as the sidebar: flag the animation before clamping so
            // the width reconcile preserves the collapsed render width instead of
            // snapping to the target and cancelling the open animation.
            self.details_width_animating = true;
            self.clamp_pane_widths_to_window();
        }

        let target = if collapsed {
            self.pane_collapsed_width()
        } else {
            self.details_width
        };
        self.animate_details_render_width_to(target, cx);
        cx.notify();
    }

    fn pane_resize_handle(
        &self,
        theme: AppTheme,
        id: &'static str,
        handle: PaneResizeHandle,
        cx: &gpui::Context<Self>,
    ) -> gpui::Stateful<gpui::Div> {
        let collapsed = match handle {
            PaneResizeHandle::Sidebar => self.sidebar_collapsed,
            PaneResizeHandle::Details => self.details_collapsed,
        };
        if collapsed {
            return div().id(id).w(px(0.0)).h_full();
        }

        // Only the details divider shows an idle hairline: it separates two
        // regions inside the content card. The sidebar handle sits on the
        // bare canvas and stays invisible until hovered or dragged.
        let idle_line = matches!(handle, PaneResizeHandle::Details);
        let dragging = self.pane_resize.is_some_and(|state| state.handle == handle);
        div()
            .id(id)
            .debug_selector(move || id.to_string())
            .group(id)
            .w(self.pane_resize_handle_width())
            .h_full()
            .cursor(CursorStyle::ResizeLeftRight)
            .child(components::resize_grip(
                theme,
                self.ui_scale_percent,
                id,
                components::ResizeGripAxis::Vertical,
                dragging,
                idle_line.then_some(theme.colors.stroke.subtle),
            ))
            .on_drag(handle, |_handle, _offset, _window, cx| {
                cx.new(|_cx| PaneResizeDragGhost)
            })
            .on_mouse_down(
                MouseButton::Left,
                cx.listener(move |this, e: &MouseDownEvent, _w, cx| {
                    cx.stop_propagation();
                    crate::press_gesture::claim_press(cx);
                    match handle {
                        PaneResizeHandle::Sidebar => {
                            this.sidebar_width_anim_seq =
                                this.sidebar_width_anim_seq.wrapping_add(1);
                            this.sidebar_width_animating = false;
                            this.sidebar_render_width = this.sidebar_width;
                        }
                        PaneResizeHandle::Details => {
                            this.details_width_anim_seq =
                                this.details_width_anim_seq.wrapping_add(1);
                            this.details_width_animating = false;
                            this.details_render_width = this.details_width;
                        }
                    }
                    this.pane_resize = Some(PaneResizeState::new(
                        handle,
                        e.position.x,
                        this.sidebar_width,
                        this.details_width,
                        this.last_window_size.width,
                        this.sidebar_collapsed,
                        this.details_collapsed,
                    ));
                    cx.notify();
                }),
            )
            .on_drag_move(cx.listener(
                move |this, e: &gpui::DragMoveEvent<PaneResizeHandle>, _w, cx| {
                    let Some(state) = this.pane_resize else {
                        return;
                    };
                    if state.handle != *e.drag(cx) {
                        return;
                    }

                    let total_w = this.last_window_size.width;
                    let next_width = next_pane_resize_drag_width(
                        &state,
                        e.event.position.x,
                        total_w,
                        this.sidebar_collapsed,
                        this.details_collapsed,
                    );
                    let mut changed = false;
                    match state.handle {
                        PaneResizeHandle::Sidebar => {
                            if this.sidebar_width != next_width {
                                this.set_sidebar_width_from_pixels(next_width);
                                changed = true;
                            }
                            if this.sidebar_render_width != next_width {
                                this.sidebar_render_width = next_width;
                                changed = true;
                            }
                        }
                        PaneResizeHandle::Details => {
                            if this.details_width != next_width {
                                this.set_details_width_from_pixels(next_width);
                                changed = true;
                            }
                            if this.details_render_width != next_width {
                                this.details_render_width = next_width;
                                changed = true;
                            }
                        }
                    }
                    if changed {
                        cx.notify();
                    }
                },
            ))
            .on_mouse_up(
                MouseButton::Left,
                cx.listener(|this, _e, _w, cx| {
                    if this.pane_resize.take().is_some() {
                        this.schedule_ui_settings_persist(cx);
                        cx.notify();
                    }
                }),
            )
            .on_mouse_up_out(
                MouseButton::Left,
                cx.listener(|this, _e, _w, cx| {
                    if this.pane_resize.take().is_some() {
                        this.schedule_ui_settings_persist(cx);
                        cx.notify();
                    }
                }),
            )
    }

    fn active_repo_id(&self) -> Option<RepoId> {
        self.state.active_repo
    }

    fn active_repo(&self) -> Option<&RepoState> {
        let repo_id = self.active_repo_id()?;
        self.state.repos.iter().find(|repo| repo.id == repo_id)
    }

    fn drive_focused_mergetool_bootstrap(&mut self) {
        if !self.state.git_runtime.is_available() {
            return;
        }

        let Some(bootstrap) = self.focused_mergetool_bootstrap.as_ref() else {
            return;
        };
        let Some(action) = focused_mergetool_bootstrap_action(&self.state, bootstrap) else {
            return;
        };

        match action {
            FocusedMergetoolBootstrapAction::OpenRepo(path) => {
                self.store.dispatch(Msg::OpenRepo(path))
            }
            FocusedMergetoolBootstrapAction::SetActiveRepo(repo_id) => {
                self.store.dispatch(Msg::SetActiveRepo { repo_id });
            }
            FocusedMergetoolBootstrapAction::SelectConflictDiff { repo_id, path } => {
                self.store
                    .dispatch(Msg::SelectConflictDiff { repo_id, path });
            }
            FocusedMergetoolBootstrapAction::LoadConflictFile { repo_id, path } => {
                self.store.dispatch(Msg::LoadConflictFile {
                    repo_id,
                    path,
                    mode: gitcomet_state::model::ConflictFileLoadMode::CurrentOnly,
                });
            }
            FocusedMergetoolBootstrapAction::Complete => {
                self.focused_mergetool_bootstrap = None;
            }
        }
    }

    pub(super) fn drive_submodule_diff_bootstrap(&mut self) {
        if !self.state.git_runtime.is_available() {
            return;
        }

        let Some(bootstrap) = self.submodule_diff_bootstrap.as_ref() else {
            return;
        };
        let Some(action) = submodule_diff_bootstrap_action(&self.state, bootstrap) else {
            return;
        };

        match action {
            SubmoduleDiffBootstrapAction::OpenRepo(path) => {
                self.store.dispatch(Msg::OpenRepo(path))
            }
            SubmoduleDiffBootstrapAction::SetActiveRepo(repo_id) => {
                self.store.dispatch(Msg::SetActiveRepo { repo_id });
            }
            SubmoduleDiffBootstrapAction::SelectDiff { repo_id, target } => {
                self.store.dispatch(Msg::SelectDiff { repo_id, target });
            }
            SubmoduleDiffBootstrapAction::Complete => {
                self.submodule_diff_bootstrap = None;
            }
        }
    }

    #[cfg(test)]
    fn remote_rows(repo: &RepoState) -> Vec<RemoteRow> {
        let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();

        if let Loadable::Ready(remote_branches) = &repo.remote_branches {
            for branch in remote_branches.iter() {
                grouped
                    .entry(branch.remote.clone())
                    .or_default()
                    .push(branch.name.clone());
            }
        }

        if grouped.is_empty()
            && let Loadable::Ready(remotes) = &repo.remotes
        {
            for remote in remotes.iter() {
                grouped.entry(remote.name.clone()).or_default();
            }
        }

        let mut rows = Vec::new();
        for (remote, mut branches) in grouped {
            branches.sort_unstable();
            branches.dedup();
            rows.push(RemoteRow::Header(remote.clone()));
            for name in branches {
                rows.push(RemoteRow::Branch {
                    remote: remote.clone(),
                    name,
                });
            }
        }

        rows
    }

    fn show_error_banner(&mut self, repo_id: Option<RepoId>, message: String) {
        if message.trim().is_empty() {
            return;
        }

        if self
            .state
            .banner_error
            .as_ref()
            .is_some_and(|banner| banner.repo_id == repo_id && banner.message == message)
        {
            return;
        }

        self.store
            .dispatch(Msg::ShowBannerError { repo_id, message });
    }

    fn split_error_banner_message(err_text: &str) -> (Option<SharedString>, SharedString) {
        let lines: Vec<&str> = err_text.lines().collect();
        let Some(cmd_start) = lines.iter().position(|line| line.starts_with("    git ")) else {
            return (None, err_text.to_string().into());
        };

        let mut cmd_end = cmd_start;
        while cmd_end < lines.len() && lines[cmd_end].starts_with("    ") {
            cmd_end += 1;
        }

        let command = lines[cmd_start..cmd_end]
            .iter()
            .map(|line| line.strip_prefix("    ").unwrap_or(line))
            .collect::<Vec<_>>()
            .join("\n");

        let mut body_lines: Vec<String> = Vec::with_capacity(lines.len());
        for line in &lines[..cmd_start] {
            body_lines.push((*line).to_string());
        }
        for line in &lines[cmd_end..] {
            body_lines.push(line.strip_prefix("    ").unwrap_or(line).to_string());
        }

        let mut collapsed: Vec<String> = Vec::with_capacity(body_lines.len());
        let mut prev_blank = false;
        for line in body_lines {
            let blank = line.trim().is_empty();
            if blank && prev_blank {
                continue;
            }
            collapsed.push(line);
            prev_blank = blank;
        }

        (Some(command.into()), collapsed.join("\n").into())
    }

    fn should_show_error_banner_overflow_hint(err_text: &str) -> bool {
        err_text.lines().count() > ERROR_BANNER_OVERFLOW_HINT_MIN_LINES
            || err_text.len() > ERROR_BANNER_OVERFLOW_HINT_MIN_CHARS
    }

    fn should_render_generic_error_banner(auth_prompt_active: bool) -> bool {
        !auth_prompt_active
    }

    fn auth_prompt_banner_colors(theme: AppTheme) -> (gpui::Rgba, gpui::Rgba) {
        (
            with_alpha(theme.colors.accent.foreground, 0.15),
            with_alpha(theme.colors.accent.foreground, 0.3),
        )
    }

    fn try_auth_prompt_submit(&mut self, cx: &mut gpui::Context<Self>) {
        let Some(prompt) = self.state.auth_prompt.as_ref() else {
            return;
        };
        let requires_username = prompt.kind == AuthPromptKind::UsernamePassword;
        let secret_required_message = match prompt.kind {
            AuthPromptKind::UsernamePassword => "Password is required.",
            AuthPromptKind::Passphrase => "Passphrase is required.",
            AuthPromptKind::HostVerification => "Confirmation is required (`yes` or fingerprint).",
        };

        let username = self
            .auth_prompt_username_input
            .read(cx)
            .text()
            .trim()
            .to_string();
        let secret = self.auth_prompt_secret_input.read(cx).text().to_string();

        if requires_username && username.is_empty() {
            self.push_toast(
                components::ToastKind::Error,
                "Username is required.".to_string(),
                cx,
            );
            return;
        }
        if secret.trim().is_empty() {
            self.push_toast(
                components::ToastKind::Error,
                secret_required_message.to_string(),
                cx,
            );
            return;
        }

        self.store.dispatch(Msg::SubmitAuthPrompt {
            username: requires_username.then_some(username),
            secret,
        });
        cx.notify();
    }

    fn push_toast(
        &mut self,
        kind: components::ToastKind,
        message: String,
        cx: &mut gpui::Context<Self>,
    ) {
        if matches!(kind, components::ToastKind::Error) {
            self.show_error_banner(self.active_repo_id(), message);
            return;
        }
        self.toast_host
            .update(cx, |host, cx| host.push_toast(kind, message, cx));
    }

    #[cfg_attr(test, allow(dead_code))]
    fn push_toast_with_link(
        &mut self,
        kind: components::ToastKind,
        message: String,
        link_url: String,
        link_label: String,
        cx: &mut gpui::Context<Self>,
    ) {
        if matches!(kind, components::ToastKind::Error) {
            self.show_error_banner(self.active_repo_id(), message);
            return;
        }
        self.toast_host.update(cx, |host, cx| {
            host.push_toast_with_link(kind, message, link_url, link_label, cx)
        });
    }

    fn active_repo_workdir(&self) -> Option<std::path::PathBuf> {
        let repo_id = self.active_repo_id()?;
        self.state
            .repos
            .iter()
            .find(|repo| repo.id == repo_id)
            .map(|repo| repo.spec.workdir.clone())
    }

    pub(crate) fn open_active_repo_in_external_code_editor(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(workdir) = self.active_repo_workdir() else {
            self.push_toast(
                components::ToastKind::Error,
                "No active repository to open in code editor.".to_string(),
                cx,
            );
            return;
        };
        self.open_path_in_external_code_editor(workdir, cx);
    }

    pub(in crate::view) fn open_path_in_external_code_editor(
        &mut self,
        path: std::path::PathBuf,
        cx: &mut gpui::Context<Self>,
    ) {
        if !path.exists() {
            self.push_toast(
                components::ToastKind::Error,
                format!("Path not found: {}", path.display()),
                cx,
            );
            return;
        }

        if let Err(err) = crate::external_editor::launch_configured_editor(&path) {
            self.push_toast(
                components::ToastKind::Error,
                format!("Failed to open in code editor: {err}"),
                cx,
            );
        }
    }

    fn report_startup_crash_report(&self) -> Result<(), std::io::Error> {
        self.report_startup_crash_report_with(platform_open::open_url)
    }

    fn report_startup_crash_report_with(
        &self,
        open_url: impl FnOnce(&str) -> Result<(), std::io::Error>,
    ) -> Result<(), std::io::Error> {
        let Some(report) = self.startup_crash_report.as_ref() else {
            return Ok(());
        };
        open_url(&report.issue_url)
    }

    fn ignore_startup_crash_report(&mut self) -> Result<(), std::io::Error> {
        let Some(report) = self.startup_crash_report.as_ref() else {
            return Ok(());
        };
        match std::fs::remove_file(&report.crash_log_path) {
            Ok(()) => {}
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
            Err(err) => return Err(err),
        }
        self.startup_crash_report = None;
        Ok(())
    }

    fn defer_text_input_main_pane_action<F>(&self, cx: &mut gpui::Context<Self>, action: F)
    where
        F: FnOnce(&mut MainPaneView, &mut Window, &mut gpui::Context<MainPaneView>) -> bool
            + 'static,
    {
        let main_pane = self.main_pane.clone();
        let window_handle = self.window_handle;
        cx.defer(move |cx| {
            let _ = window_handle.update(cx, |_, window, cx| {
                main_pane.update(cx, |pane, cx| {
                    if action(pane, window, cx) {
                        cx.notify();
                        window.refresh();
                    }
                });
            });
        });
    }

    fn defer_text_input_adjacent_diff_file_navigation(
        &self,
        direction: i8,
        cx: &mut gpui::Context<Self>,
    ) {
        self.defer_text_input_main_pane_action(cx, move |pane, window, cx| {
            let Some(repo_id) = pane.active_repo_id() else {
                return false;
            };
            pane.try_select_adjacent_diff_file_preserving_focus(repo_id, direction, window, cx)
        });
    }

    fn defer_adjacent_diff_file_navigation(&self, direction: i8, cx: &mut gpui::Context<Self>) {
        self.defer_text_input_main_pane_action(cx, move |pane, window, cx| {
            let Some(repo_id) = pane.active_repo_id() else {
                return false;
            };
            pane.try_select_adjacent_diff_file(repo_id, direction, window, cx)
        });
    }

    /// Mouse back/forward side buttons: step the active repo's global navigation
    /// history (diffs, file content, commit selections). Active anywhere in the
    /// window.
    fn dispatch_global_nav(&self, forward: bool, cx: &mut gpui::Context<Self>) {
        let Some(repo_id) = self.main_pane.read(cx).active_repo_id() else {
            return;
        };
        let msg = if forward {
            Msg::GlobalNavForward { repo_id }
        } else {
            Msg::GlobalNavBack { repo_id }
        };
        self.store.dispatch(msg);
        cx.notify();
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn is_popover_open(&self, app: &App) -> bool {
        self.popover_host.read(app).is_open()
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn tooltip_host_for_test(&self) -> Entity<TooltipHost> {
        self.tooltip_host.clone()
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn tooltip_text_for_test(&self, app: &App) -> Option<SharedString> {
        self.tooltip_host
            .read(app)
            .tooltip_text_for_test()
            .or_else(tooltip::tooltip_text_for_test)
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn open_repo_panel_visible_for_test(&self) -> bool {
        self.open_repo_panel
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn show_timezone_for_test(&self) -> bool {
        self.show_timezone
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(in crate::view) fn change_tracking_view_for_test(&self) -> ChangeTrackingView {
        self.change_tracking_view
    }

    #[cfg(test)]
    pub(in crate::view) fn terminal_preferences_for_test(&self) -> &TerminalPreferences {
        &self.terminal_preferences
    }

    fn resume_after_git_runtime_recovery(&mut self) {
        if let Some(bootstrap) = self.deferred_repo_bootstrap.take() {
            match bootstrap {
                DeferredRepoBootstrap::RestoreSession {
                    open_repos,
                    active_repo,
                } => {
                    self.startup_repo_bootstrap_pending = true;
                    self.store.dispatch(Msg::RestoreSession {
                        open_repos,
                        active_repo,
                    });
                }
                DeferredRepoBootstrap::OpenRepo(path) => {
                    self.startup_repo_bootstrap_pending = true;
                    self.store.dispatch(Msg::OpenRepo(path));
                }
            }
            return;
        }

        if !self.state.repos.is_empty() {
            let repo_ids: Vec<_> = self.state.repos.iter().map(|repo| repo.id).collect();
            for repo_id in repo_ids {
                self.store.dispatch(Msg::ReloadRepo { repo_id });
            }
        }
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(in crate::view) fn diff_scroll_sync_for_test(&self) -> DiffScrollSync {
        self.diff_scroll_sync
    }
}

impl Render for GitCometView {
    fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
        #[cfg(test)]
        clear_visible_tooltip_text_for_test();

        let theme = self.theme;
        let font_preferences = crate::font_preferences::current(cx);
        debug_assert!(matches!(
            self.view_mode,
            GitCometViewMode::Normal | GitCometViewMode::FocusedMergetool
        ));
        self.last_window_size = window.viewport_size();
        self.clamp_pane_widths_to_window();
        if self.last_window_size != self.ui_window_size_last_seen {
            self.ui_window_size_last_seen = self.last_window_size;
            self.schedule_ui_settings_persist(cx);
        }

        if let Some(repo_id) = self.pending_pull_reconcile_prompt.take()
            && self.active_repo_id() == Some(repo_id)
        {
            self.open_popover_at(
                PopoverKind::PullReconcilePrompt { repo_id },
                self.last_mouse_pos,
                window,
                cx,
            );
        }

        if let Some(prompt) = self.pending_unsaved_file_edits_prompt.take() {
            let anchor = point(
                self.last_window_size.width / 2.0,
                self.last_window_size.height / 2.0,
            );
            self.open_popover_at(
                PopoverKind::UnsavedFileEditsConfirm(prompt),
                anchor,
                window,
                cx,
            );
        }

        if let Some(prompt) = self.pending_terminal_shutdown_prompt.take() {
            let anchor = point(
                self.last_window_size.width / 2.0,
                self.last_window_size.height / 2.0,
            );
            self.open_popover_at(
                PopoverKind::TerminalShutdownConfirm(prompt),
                anchor,
                window,
                cx,
            );
        }

        if let Some((repo_id, name)) = self.pending_force_delete_branch_prompt.take()
            && self.active_repo_id() == Some(repo_id)
        {
            if self.pending_force_delete_branch_centered {
                self.open_popover_centered(
                    PopoverKind::ForceDeleteBranchConfirm { repo_id, name },
                    window,
                    cx,
                );
            } else {
                self.open_popover_at(
                    PopoverKind::ForceDeleteBranchConfirm { repo_id, name },
                    self.last_mouse_pos,
                    window,
                    cx,
                );
            }
        }

        if let Some((repo_id, path, branch)) = self.pending_force_remove_worktree_prompt.take()
            && self.active_repo_id() == Some(repo_id)
        {
            self.open_popover_at(
                PopoverKind::ForceRemoveWorktreeConfirm {
                    repo_id,
                    path,
                    branch,
                },
                self.last_mouse_pos,
                window,
                cx,
            );
        }

        // A trust check just started: open the trust popover immediately in its
        // pending/spinner state so there is no dead gap while the background
        // check runs. It fills in with the real sources (or is closed on a
        // silent proceed) when the check resolves — see `apply_state_snapshot`.
        if let Some(check) = self.pending_submodule_trust_check.take()
            && self.active_repo_id() == Some(check.repo_id)
        {
            self.open_popover_at(
                PopoverKind::submodule(check.repo_id, SubmodulePopoverKind::TrustConfirm),
                self.last_mouse_pos,
                window,
                cx,
            );
        }

        if let Some(prompt) = self.pending_submodule_trust_prompt.take()
            && self.active_repo_id() == Some(prompt.repo_id)
        {
            self.open_popover_at(
                PopoverKind::submodule(prompt.repo_id, SubmodulePopoverKind::TrustConfirm),
                self.last_mouse_pos,
                window,
                cx,
            );
        }

        let decorations = window.window_decorations();
        let (tiling, client_inset) = match decorations {
            Decorations::Client { tiling } => (
                Some(tiling),
                chrome::client_side_decoration_inset(self.ui_scale_percent),
            ),
            Decorations::Server => (None, px(0.0)),
        };
        window.set_client_inset(client_inset);

        let cursor = self
            .hover_resize_edge
            .map(cursor_style_for_resize_edge)
            .unwrap_or(CursorStyle::Arrow);

        let center_content = self.center_content(window, cx);
        let font_features = crate::font_preferences::current_font_features(cx);
        let show_custom_window_chrome =
            crate::linux_gui_env::LinuxGuiEnvironment::should_render_custom_window_chrome(
                decorations,
            );

        let mut body = div()
            .flex()
            .flex_col()
            .size_full()
            .font(gpui::Font {
                family: crate::font_preferences::applied_ui_font_family(
                    &font_preferences.ui_font_family,
                )
                .into(),
                features: font_features,
                fallbacks: None,
                weight: gpui::FontWeight::default(),
                style: gpui::FontStyle::default(),
            })
            .text_color(theme.colors.foreground.primary)
            // Any click anywhere hides visible tooltips (both gpui-managed
            // bubbles and the canvas-driven TooltipHost overlay).
            .capture_any_mouse_down(cx.listener(|this, _e: &MouseDownEvent, _window, cx| {
                tooltip::dismiss_tooltips_on_mouse_down(cx);
                this.tooltip_host.update(cx, |host, cx| {
                    host.clear_tooltip(cx);
                });
                this.commit_message_hover_host
                    .update(cx, |host, cx| host.dismiss(cx));
            }));

        if show_custom_window_chrome {
            body = body.child(stable_cached_fixed_height_view(
                self.title_bar.clone(),
                chrome::title_bar_height(self.ui_scale_percent),
            ));
        }

        body = body.child(center_content);

        if let Some(report) = self.startup_crash_report.clone()
            && self.view_mode == GitCometViewMode::Normal
        {
            let summary = report.summary.clone();

            let report_button =
                components::Button::new("startup_crash_report_open", "Report Issue")
                    .style(components::ButtonStyle::Filled)
                    .on_click(theme, cx, |this, _e, _w, cx| {
                        match this.report_startup_crash_report() {
                            Ok(()) => this.push_toast(
                                components::ToastKind::Success,
                                "Opened crash report page in your browser.".to_string(),
                                cx,
                            ),
                            Err(err) => {
                                this.push_toast(
                                    components::ToastKind::Error,
                                    format!("Failed to open browser: {err}"),
                                    cx,
                                );
                            }
                        }
                        cx.notify();
                    });

            let ignore_button =
                components::Button::new("startup_crash_report_ignore", "Ignore Crash")
                    .style(components::ButtonStyle::Outlined)
                    .on_click(theme, cx, |this, _e, _w, cx| {
                        if let Err(err) = this.ignore_startup_crash_report() {
                            this.push_toast(
                                components::ToastKind::Error,
                                format!("Could not clear crash report: {err}"),
                                cx,
                            );
                        }
                        cx.notify();
                    });

            body = body.child(
                div()
                    .id("startup_crash_report")
                    .debug_selector(|| "startup_crash_report".to_string())
                    .relative()
                    .px_2()
                    .py_1()
                    // Light's `status.*.background` is a saturated cream that
                    // reads as a coloured card rather than a notification. The
                    // status colour stays in the border; the panel is neutral,
                    // like the toasts and the progress shell.
                    .bg(if theme.is_dark {
                        with_alpha(theme.colors.status.warning.foreground, 0.13)
                    } else {
                        theme.colors.surface.raised
                    })
                    .border_1()
                    .border_color(if theme.is_dark {
                        with_alpha(theme.colors.status.warning.foreground, 0.30)
                    } else {
                        theme.colors.status.warning.border
                    })
                    .rounded(px(theme.radii.panel))
                    .child(
                        div()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .child(
                                div()
                                    .text_sm()
                                    .font_weight(FontWeight::BOLD)
                                    .child("GitComet recovered from program crash"),
                            )
                            .child(
                                div()
                                    .text_sm()
                                    .text_color(theme.colors.foreground.secondary)
                                    .child(
                                        "Would you like to contribute by reporting issue to GitComet GitHub repository?",
                                    ),
                            )
                            .child(
                                div()
                                    .text_xs()
                                    .text_color(theme.colors.foreground.secondary)
                                    .child(format!("Summary: {summary}")),
                            )
                            .child(
                                div()
                                    .pt_1()
                                    .flex()
                                    .items_center()
                                    .gap_1()
                                    .child(report_button)
                                    .child(ignore_button),
                            ),
                    ),
            );
        }

        if let Some(prompt) = self.state.auth_prompt.clone() {
            let prompt_key = format!("{:?}:{:?}", prompt.kind, prompt.operation);
            if self.auth_prompt_key.as_ref() != Some(&prompt_key) {
                self.auth_prompt_key = Some(prompt_key);
                self.auth_prompt_username_input
                    .update(cx, |input, cx| input.set_text("", cx));
                self.auth_prompt_secret_input
                    .update(cx, |input, cx| input.set_text("", cx));
            }

            self.auth_prompt_username_input
                .update(cx, |input, cx| input.set_theme(theme, cx));
            let is_host_verification = prompt.kind == AuthPromptKind::HostVerification;
            self.auth_prompt_secret_input.update(cx, |input, cx| {
                input.set_theme(theme, cx);
                input.set_masked(!is_host_verification, cx);
            });

            let requires_username = prompt.kind == AuthPromptKind::UsernamePassword;
            let title = match prompt.kind {
                AuthPromptKind::UsernamePassword => "Repository authentication required",
                AuthPromptKind::Passphrase => "Passphrase required",
                AuthPromptKind::HostVerification => "Host authenticity confirmation required",
            };
            let subtitle = match prompt.kind {
                AuthPromptKind::UsernamePassword => {
                    "Enter username and password, then confirm to retry."
                }
                AuthPromptKind::Passphrase => "Enter your key passphrase, then confirm to retry.",
                AuthPromptKind::HostVerification => {
                    "Enter `yes` to trust this host key, or paste the shown fingerprint."
                }
            };

            let confirm_button = components::Button::new("auth_prompt_confirm", "Confirm")
                .style(components::ButtonStyle::Filled)
                .on_click(theme, cx, move |this, _e, _w, cx| {
                    this.try_auth_prompt_submit(cx);
                });

            let cancel_button = components::Button::new("auth_prompt_cancel", "Cancel")
                .style(components::ButtonStyle::Outlined)
                .on_click(theme, cx, |this, _e, _w, cx| {
                    this.store.dispatch(Msg::CancelAuthPrompt);
                    cx.notify();
                });

            let prompt_form = div()
                .flex()
                .flex_col()
                .gap_1()
                .child(div().text_sm().font_weight(FontWeight::BOLD).child(title))
                .child(
                    div()
                        .text_sm()
                        .text_color(theme.colors.foreground.secondary)
                        .child(subtitle),
                )
                .when(requires_username, |this| {
                    this.child(self.auth_prompt_username_input.clone())
                })
                .child(self.auth_prompt_secret_input.clone())
                .when(is_host_verification, |this| {
                    this.child(
                        div()
                            .text_xs()
                            .text_color(theme.colors.foreground.secondary)
                            .child("Use Cancel if you do not trust this host."),
                    )
                })
                .when(!prompt.reason.trim().is_empty(), |this| {
                    this.child(
                        restrict_scroll_to_vertical_axis(
                            div()
                                .id("auth_prompt_reason_scroll")
                                .max_h(px(96.0))
                                .overflow_y_scroll(),
                        )
                        .child(
                            div()
                                .text_xs()
                                .text_color(theme.colors.foreground.secondary)
                                .child(prompt.reason.clone()),
                        ),
                    )
                })
                .child(
                    div()
                        .pt_1()
                        .flex()
                        .items_center()
                        .gap_1()
                        .child(confirm_button)
                        .child(cancel_button),
                );

            let (prompt_bg, prompt_border) = Self::auth_prompt_banner_colors(theme);
            body = body.child(
                div()
                    .relative()
                    .px_2()
                    .py_1()
                    .bg(prompt_bg)
                    .border_1()
                    .border_color(prompt_border)
                    .rounded(px(theme.radii.panel))
                    .child(prompt_form),
            );
        } else {
            self.auth_prompt_key = None;
        }

        let banner_error =
            if Self::should_render_generic_error_banner(self.state.auth_prompt.is_some()) {
                self.state
                    .banner_error
                    .as_ref()
                    .map(|banner| banner.message.clone())
            } else {
                None
            };
        if let Some(err_text) = banner_error {
            let (error_command, display_error) =
                Self::split_error_banner_message(err_text.as_ref());
            let show_overflow_hint =
                Self::should_show_error_banner_overflow_hint(err_text.as_ref());
            self.error_banner_input.update(cx, |input, cx| {
                input.set_theme(theme, cx);
                input.set_text(display_error.clone(), cx);
                input.set_read_only(true, cx);
            });

            let dismiss = components::Button::new("repo_error_banner_close", "")
                .start_slot(svg_icon(
                    "icons/generic_close.svg",
                    theme.colors.foreground.secondary,
                    px(12.0),
                ))
                .style(components::ButtonStyle::Transparent)
                .on_click(theme, cx, move |this, _e, _w, _cx| {
                    this.store.dispatch(Msg::DismissBannerError);
                });

            let command_block = error_command.as_ref().map(|command| {
                div()
                    .id("repo_error_banner_command")
                    .font_family(crate::font_preferences::EDITOR_MONOSPACE_FONT_FAMILY)
                    .bg(with_alpha(
                        theme.colors.surface.canvas,
                        if theme.is_dark { 0.28 } else { 0.75 },
                    ))
                    .rounded(px(theme.radii.row))
                    .px_2()
                    .py_1()
                    .child(command.clone())
            });

            body = body.child(
                div()
                    .relative()
                    .px_2()
                    .py_1()
                    .pr(px(40.0))
                    .bg(if theme.is_dark {
                        with_alpha(theme.colors.status.danger.foreground, 0.15)
                    } else {
                        theme.colors.surface.raised
                    })
                    .border_1()
                    .border_color(if theme.is_dark {
                        with_alpha(theme.colors.status.danger.foreground, 0.3)
                    } else {
                        theme.colors.status.danger.border
                    })
                    .rounded(px(theme.radii.panel))
                    .child(
                        restrict_scroll_to_vertical_axis(
                            div()
                                .id("repo_error_banner_scroll")
                                .max_h(px(140.0))
                                .overflow_y_scroll(),
                        )
                        .child(
                            div()
                                .flex()
                                .flex_col()
                                .gap_1()
                                .when_some(command_block, |this, command_block| {
                                    this.child(command_block)
                                })
                                .child(self.error_banner_input.clone()),
                        ),
                    )
                    .when(show_overflow_hint, |this| {
                        this.child(
                            div()
                                .mt_1()
                                .text_xs()
                                .text_color(theme.colors.foreground.secondary)
                                .child("Scroll for full output"),
                        )
                    })
                    .child(div().absolute().top(px(6.0)).right(px(6.0)).child(dismiss)),
            );
        }

        let mut root = div()
            .size_full()
            .cursor(cursor)
            .text_color(theme.colors.foreground.primary);
        root = root.relative();
        root = root.child(UiScaleScrollCapture { view: cx.entity() });
        root = root
            .on_action(cx.listener(|this, _: &OpenActiveViewSearch, window, cx| {
                let handled = this
                    .main_pane
                    .update(cx, |pane, cx| pane.open_search_for_active_view(window, cx));
                if handled {
                    cx.stop_propagation();
                }
            }))
            .on_action(cx.listener(|this, _: &ToggleCommandPalette, window, cx| {
                if !command_palette_available(this.view_mode) {
                    cx.stop_propagation();
                    return;
                }
                this.toggle_command_palette(window, cx);
                cx.stop_propagation();
            }))
            .on_action(cx.listener(|this, _: &LocateFileInExplorer, _window, cx| {
                this.locate_open_file_in_explorer(cx);
                cx.stop_propagation();
            }))
            .on_action(cx.listener(|this, _: &CommandPaletteDismiss, window, cx| {
                if this.command_palette_open {
                    this.close_command_palette(window, cx);
                    cx.stop_propagation();
                }
            }))
            .on_action(cx.listener(|this, _: &TextInputCommitSubmit, window, cx| {
                let handled = this.details_pane.update(cx, |pane, cx| {
                    pane.handle_commit_submit_shortcut(window, cx)
                });
                if handled {
                    cx.stop_propagation();
                }
            }))
            .on_action(cx.listener(|this, _: &TextInputDiffPrevFile, _window, cx| {
                if !show_diff_file_navigation(this.view_mode) {
                    cx.stop_propagation();
                    return;
                }
                this.defer_text_input_adjacent_diff_file_navigation(-1, cx);
                cx.stop_propagation();
            }))
            .on_action(cx.listener(|this, _: &TextInputDiffNextFile, _window, cx| {
                if !show_diff_file_navigation(this.view_mode) {
                    cx.stop_propagation();
                    return;
                }
                this.defer_text_input_adjacent_diff_file_navigation(1, cx);
                cx.stop_propagation();
            }))
            .on_action(cx.listener(
                |this, _: &TextInputDiffPrevSearchMatchOrChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_prev_search_match_or_diff_change(cx)
                    });
                    cx.stop_propagation();
                },
            ))
            .on_action(cx.listener(
                |this, _: &TextInputDiffNextSearchMatchOrChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_next_search_match_or_diff_change(cx)
                    });
                    cx.stop_propagation();
                },
            ))
            .on_action(cx.listener(|this, _: &DiffPrevFile, _window, cx| {
                if !show_diff_file_navigation(this.view_mode) {
                    cx.stop_propagation();
                    return;
                }
                this.defer_adjacent_diff_file_navigation(-1, cx);
                cx.stop_propagation();
            }))
            .on_action(cx.listener(|this, _: &DiffNextFile, _window, cx| {
                if !show_diff_file_navigation(this.view_mode) {
                    cx.stop_propagation();
                    return;
                }
                this.defer_adjacent_diff_file_navigation(1, cx);
                cx.stop_propagation();
            }))
            .on_action(
                cx.listener(|this, _: &DiffPrevSearchMatchOrChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_prev_search_match_or_diff_change(cx)
                    });
                    cx.stop_propagation();
                }),
            )
            .on_action(
                cx.listener(|this, _: &DiffNextSearchMatchOrChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_next_search_match_or_diff_change(cx)
                    });
                    cx.stop_propagation();
                }),
            )
            .on_action(
                cx.listener(|this, _: &TextInputDiffPrevChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_prev_diff_change(cx)
                    });
                    cx.stop_propagation();
                }),
            )
            .on_action(
                cx.listener(|this, _: &TextInputDiffNextChange, _window, cx| {
                    this.defer_text_input_main_pane_action(cx, |pane, _window, cx| {
                        pane.navigate_next_diff_change(cx)
                    });
                    cx.stop_propagation();
                }),
            );

        root = root.on_mouse_move(cx.listener(|this, e: &MouseMoveEvent, window, cx| {
            this.last_mouse_pos = e.position;
            this.history_refs_hover_host
                .update(cx, |host, cx| host.on_mouse_moved(e.position, cx));
            this.commit_message_hover_host
                .update(cx, |host, cx| host.on_mouse_moved(e.position, cx));
            this.tooltip_host
                .update(cx, |tooltip, cx| tooltip.on_mouse_moved(e.position, cx));

            let Decorations::Client { tiling } = window.window_decorations() else {
                if this.hover_resize_edge.is_some() {
                    this.hover_resize_edge = None;
                    cx.notify();
                }
                return;
            };

            let size = window.viewport_size();
            let next = resize_edge(
                e.position,
                chrome::client_side_decoration_inset(this.ui_scale_percent),
                size,
                tiling,
            );
            if next != this.hover_resize_edge {
                this.hover_resize_edge = next;
                cx.notify();
            }
        }));
        root = root.on_any_mouse_down(cx.listener(|this, _e: &MouseDownEvent, _window, cx| {
            this.dismiss_history_refs_menus(cx);
        }));
        root = root
            .on_mouse_down(
                MouseButton::Navigate(gpui::NavigationDirection::Back),
                cx.listener(|this, _e: &MouseDownEvent, _window, cx| {
                    this.dispatch_global_nav(false, cx);
                }),
            )
            .on_mouse_down(
                MouseButton::Navigate(gpui::NavigationDirection::Forward),
                cx.listener(|this, _e: &MouseDownEvent, _window, cx| {
                    this.dispatch_global_nav(true, cx);
                }),
            );
        if tiling.is_some() {
            root = root.on_mouse_down(
                MouseButton::Left,
                cx.listener(|this, e: &MouseDownEvent, window, cx| {
                    let Decorations::Client { tiling } = window.window_decorations() else {
                        return;
                    };

                    let size = window.viewport_size();
                    let edge = resize_edge(
                        e.position,
                        chrome::client_side_decoration_inset(this.ui_scale_percent),
                        size,
                        tiling,
                    );
                    let Some(edge) = edge else {
                        return;
                    };

                    cx.stop_propagation();
                    crate::app::begin_window_resize(window, edge);
                }),
            );
        } else if self.hover_resize_edge.is_some() {
            self.hover_resize_edge = None;
        }

        let framed_content = div().relative().size_full().child(body);

        let frame_overlay = div()
            .absolute()
            .top_0()
            .left_0()
            .size_full()
            .child(self.command_palette.clone())
            .child(stable_overlay_view(self.history_refs_hover_host.clone()))
            .child(stable_overlay_view(self.commit_message_hover_host.clone()))
            .child(stable_overlay_view(self.popover_host.clone()))
            .child(stable_overlay_view(self.toast_host.clone()))
            .child(stable_overlay_view(self.tooltip_host.clone()));

        root = root.child(chrome::window_frame(
            theme,
            decorations,
            framed_content.into_any_element(),
            Some(frame_overlay.into_any_element()),
            self.ui_scale_percent,
        ));

        if crate::startup_probe::is_enabled() {
            root = root.on_children_prepainted(|_children_bounds, window, _cx| {
                if crate::startup_probe::mark_first_paint() {
                    window.on_next_frame(|_window, cx| {
                        crate::startup_probe::mark_first_interactive();
                        if crate::startup_probe::should_exit_after_first_interactive() {
                            crate::app::mark_clean_shutdown_requested(cx);
                            cx.quit();
                        }
                    });
                }
            });
        }

        root
    }
}

#[cfg(test)]
mod tests;
