mod generation;
mod git;
mod model_manager;
mod preferences;
mod projects;
mod view;

pub(crate) use view::app_theme;

use generation::ChatMessage;
use git::{ActiveGitOperation, GitDiff, GitWorktree};
use model_manager::{ActiveDownload, ModelDownload, ModelOperation};
use preferences::PreferenceDraft;
#[cfg(test)]
use preferences::{parse_optional_gib, parse_streaming_cache};

use crate::config::{
    Config, DevBrainConfig, EndpointConfig, GitConfig, GitDiffAlgorithm, GitDiffLayout,
    GitDiffWhitespace, ModelPreferences, PermissionMode,
};
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage, SystemMessage};
#[cfg(any(target_os = "macos", test))]
use crate::engine::ChatTurn;
#[cfg(target_os = "macos")]
use crate::metrics::WorkSource;
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
use crate::model::{DownloadOutcome, DownloadProgress, ManagedArtifactId, ModelChoice};
#[cfg(target_os = "macos")]
use crate::runtime::{
    ActiveGeneration, CheckpointTarget, CompactionInput, GenerationEvent, GenerationService,
};
use crate::settings::{
    DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, KvCachePreferences,
    ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
    StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable, text_editor};
use iced::{Size, Subscription, Task, keyboard, mouse, window};
use rfd::AsyncFileDialog;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, TryRecvError};
#[cfg(target_os = "macos")]
use std::sync::{Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};

const APP_ID: &str = "de.rfc1437.ds4server";
const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_millis(200);
/// Disc scans are cheap but not free; the explorer does not need 5 Hz.
const CACHE_SCAN_INTERVAL: Duration = Duration::from_secs(2);
const GIT_SCAN_INTERVAL: Duration = Duration::from_secs(2);
pub(super) const MIN_SIDEBAR_WIDTH: i32 = 180;
pub(super) const MAX_SIDEBAR_WIDTH: i32 = 520;

pub(crate) struct App {
    main_window: window::Id,
    pub(super) preferences_window: Option<window::Id>,
    pub(super) model_manager_window: Option<window::Id>,
    pub(super) help_window: Option<window::Id>,
    pub(super) pending_model_delete: Option<ManagedArtifactId>,
    #[cfg(target_os = "macos")]
    _native_menu: Option<crate::native_menu::NativeMenu>,
    #[cfg(target_os = "macos")]
    pub(super) native_edit_commands: crate::native_edit::EditCommandQueue,
    #[cfg(target_os = "macos")]
    pub(super) native_edit_availability: crate::native_edit::EditAvailabilityState,
    database: Option<Database>,
    projects: Vec<ProjectWithSessions>,
    config: Config,
    preference_draft: PreferenceDraft,
    preference_error: Option<String>,
    restore_dev_brain_confirmation: bool,
    selected_project: Option<i32>,
    selected_session: Option<i32>,
    pub(super) permission_mode: PermissionMode,
    /// Unsaved sessions, keyed by project. A draft only becomes a `sessions` row
    /// when its first chat turn is stored, so empty ones vanish on restart.
    drafts: HashMap<i32, String>,
    git_states: HashMap<i32, GitState>,
    git_worktrees: HashMap<i32, GitWorktree>,
    git_selected_project: Option<i32>,
    git_selected_files: HashSet<PathBuf>,
    git_commit_message: String,
    git_diff: Option<GitDiff>,
    git_diff_layout: GitDiffLayout,
    git_commit_all_confirmation: bool,
    git_operation: Option<ActiveGitOperation>,
    last_git_scan: Instant,
    #[cfg(target_os = "macos")]
    background_chats: HashMap<i32, ChatSnapshot>,
    /// Session whose quick-actions menu is open.
    session_menu: Option<i32>,
    /// Session waiting for explicit confirmation before deletion.
    pending_session_delete: Option<i32>,
    /// Session being renamed, with the in-progress title.
    session_rename: Option<(i32, String)>,
    /// Projects whose archived sessions are expanded in the sidebar.
    expanded_archives: HashSet<i32>,
    /// True while the sidebar divider is being dragged.
    sidebar_drag: bool,
    choosing_folder: bool,
    pending_project_path: Option<PathBuf>,
    project_name_input: String,
    model_download: ModelDownload,
    pub(super) composer: text_editor::Content,
    pub(super) queued_inputs: VecDeque<String>,
    pub(super) conversation: Vec<ChatMessage>,
    /// Follow appended chat content until the user scrolls away from the tail.
    pub(super) chat_follow_tail: bool,
    active_turn: Option<generation::TurnSummary>,
    pub(super) a2ui: crate::a2ui::Store,
    pub(super) a2ui_history: Vec<crate::a2ui::Store>,
    pub(super) a2ui_history_index: Option<usize>,
    pub(super) a2ui_tabs: HashMap<(String, String), usize>,
    pub(super) a2ui_modals: HashSet<(String, String)>,
    pub(super) a2ui_editors: HashMap<(String, String, String), text_editor::Content>,
    pub(super) a2ui_markdown: HashMap<(String, String, String), markdown::Content>,
    pub(super) help_markdown: markdown::Content,
    pub(super) a2ui_choice_filters: HashMap<(String, String, String), String>,
    pub(super) a2ui_images: HashMap<String, iced::widget::image::Handle>,
    pub(super) a2ui_image_requests: HashSet<String>,
    pub(super) a2ui_image_loading: bool,
    pub(super) pending_a2ui_dismissal: Option<String>,
    pub(super) a2ui_auto_switch_pending: bool,
    pub(super) generating: bool,
    pub(super) context_used: u32,
    pub(super) context_limit: u32,
    pub(super) tokens_per_second: Option<f32>,
    pub(super) detail_tab: DetailTab,
    metrics: Arc<Metrics>,
    pub(super) metrics_snapshot: MetricsSnapshot,
    pub(super) metrics_history: VecDeque<MetricsPoint>,
    /// Disc usage of the KV cache directories, rescanned while Stats is open.
    pub(super) kv_cache_report: KvCacheReport,
    last_cache_scan: Instant,
    last_http_requests: u64,
    last_ssd_selected_requests: u64,
    last_ssd_requested_bytes: u64,
    last_ssd_wait_ms: u64,
    #[cfg(target_os = "macos")]
    generation_service: Option<GenerationService>,
    #[cfg(target_os = "macos")]
    active_generation: Option<ActiveGeneration>,
    #[cfg(target_os = "macos")]
    active_compaction: Option<generation::CompactionRequest>,
    #[cfg(target_os = "macos")]
    active_tool_check: Option<generation::ToolResultCheck>,
    #[cfg(target_os = "macos")]
    agent_tools: Option<(i32, Arc<Mutex<crate::agent::Tools>>)>,
    #[cfg(target_os = "macos")]
    active_tools: Option<crate::agent::ActiveTools>,
    #[cfg(target_os = "macos")]
    pub(super) tool_cards: Vec<crate::agent::ToolCard>,
    #[cfg(target_os = "macos")]
    pub(super) pending_tool_approval:
        Option<(crate::agent::ApprovalPrompt, std::sync::mpsc::Sender<bool>)>,
    #[cfg(target_os = "macos")]
    active_titling: Option<generation::TitleRequest>,
    #[cfg(target_os = "macos")]
    runtime_config: Arc<RwLock<Config>>,
    #[cfg(target_os = "macos")]
    _endpoint: Option<crate::server::ServerHandle>,
    error: Option<String>,
    pub(super) activity: Option<String>,
    pub(super) context_notice: Option<String>,
    stop_requested: bool,
    system_prompt_seen_at: u32,
    manual_compaction_queued: bool,
    #[cfg(target_os = "macos")]
    skip_compaction_once: bool,
    quit_confirmation: bool,
}

#[cfg(target_os = "macos")]
struct ChatSnapshot {
    selected_project: Option<i32>,
    selected_session: Option<i32>,
    permission_mode: PermissionMode,
    composer: text_editor::Content,
    queued_inputs: VecDeque<String>,
    conversation: Vec<ChatMessage>,
    chat_follow_tail: bool,
    active_turn: Option<generation::TurnSummary>,
    a2ui: crate::a2ui::Store,
    a2ui_history: Vec<crate::a2ui::Store>,
    a2ui_history_index: Option<usize>,
    a2ui_tabs: HashMap<(String, String), usize>,
    a2ui_modals: HashSet<(String, String)>,
    a2ui_editors: HashMap<(String, String, String), text_editor::Content>,
    a2ui_markdown: HashMap<(String, String, String), markdown::Content>,
    a2ui_choice_filters: HashMap<(String, String, String), String>,
    a2ui_images: HashMap<String, iced::widget::image::Handle>,
    a2ui_image_requests: HashSet<String>,
    a2ui_image_loading: bool,
    pending_a2ui_dismissal: Option<String>,
    a2ui_auto_switch_pending: bool,
    generating: bool,
    context_used: u32,
    context_limit: u32,
    tokens_per_second: Option<f32>,
    detail_tab: DetailTab,
    active_generation: Option<ActiveGeneration>,
    active_compaction: Option<generation::CompactionRequest>,
    active_tool_check: Option<generation::ToolResultCheck>,
    agent_tools: Option<(i32, Arc<Mutex<crate::agent::Tools>>)>,
    active_tools: Option<crate::agent::ActiveTools>,
    tool_cards: Vec<crate::agent::ToolCard>,
    pending_tool_approval: Option<(crate::agent::ApprovalPrompt, std::sync::mpsc::Sender<bool>)>,
    active_titling: Option<generation::TitleRequest>,
    error: Option<String>,
    activity: Option<String>,
    context_notice: Option<String>,
    stop_requested: bool,
    system_prompt_seen_at: u32,
    manual_compaction_queued: bool,
    skip_compaction_once: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct ProjectChoice {
    id: i32,
    name: String,
}

impl std::fmt::Display for ProjectChoice {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.name)
    }
}

#[derive(Clone, Debug)]
pub(super) struct GitState {
    pub(super) current: Option<String>,
    pub(super) label: String,
    pub(super) branches: Vec<String>,
}

#[cfg(target_os = "macos")]
impl ChatSnapshot {
    fn needs_poll(&self) -> bool {
        chat_needs_poll(self.generating, self.active_titling.is_some())
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) enum DetailTab {
    #[default]
    Chat,
    A2ui,
    Git,
    Stats,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum PreferenceSection {
    Model,
    Endpoint,
    DevBrain,
    Git,
    Prompt,
    Generation,
    Execution,
    Acceleration,
    KvCache,
    Steering,
}

impl PreferenceSection {
    const ALL: [Self; 10] = [
        Self::Model,
        Self::Endpoint,
        Self::DevBrain,
        Self::Git,
        Self::Prompt,
        Self::Generation,
        Self::Execution,
        Self::Acceleration,
        Self::KvCache,
        Self::Steering,
    ];

    fn anchor(self) -> &'static str {
        match self {
            Self::Model => "preferences-model",
            Self::Endpoint => "preferences-endpoint",
            Self::DevBrain => "preferences-dev-brain",
            Self::Git => "preferences-git",
            Self::Prompt => "preferences-prompt",
            Self::Generation => "preferences-generation",
            Self::Execution => "preferences-execution",
            Self::Acceleration => "preferences-acceleration",
            Self::KvCache => "preferences-kv-cache",
            Self::Steering => "preferences-steering",
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Model => "Model & lifecycle",
            Self::Endpoint => "Local endpoint",
            Self::DevBrain => "Dev Brain",
            Self::Git => "Git diffs",
            Self::Prompt => "Prompt",
            Self::Generation => "Generation",
            Self::Execution => "Execution",
            Self::Acceleration => "Acceleration & memory",
            Self::KvCache => "KV cache",
            Self::Steering => "Steering & diagnostics",
        }
    }
}

#[derive(Clone, Copy, Debug, Default)]
pub(super) struct MetricsPoint {
    pub(super) decode_tokens_per_second: f32,
    pub(super) prefill_tokens_per_second: f32,
    pub(super) http_requests_per_second: f32,
    pub(super) kv_read_bytes_per_second: f32,
    pub(super) kv_write_bytes_per_second: f32,
    pub(super) ssd_requests_per_second: f32,
    pub(super) ssd_bytes_per_second: f32,
    pub(super) ssd_wait_ms_per_second: f32,
}

#[derive(Debug, Clone)]
pub(crate) enum Message {
    Noop,
    #[cfg(target_os = "macos")]
    NativeEdit(crate::native_edit::EditCommand),
    OpenPreferences,
    PreferencesOpened(window::Id),
    ClosePreferences,
    ScrollPreferences(PreferenceSection),
    OpenModelManager,
    OpenHelp,
    HelpOpened(window::Id),
    NewChat,
    ExportChat,
    ExportChatPicked(Option<PathBuf>, String),
    ModelManagerOpened(window::Id),
    WindowOpened(window::Id),
    WindowEvent(window::Id, window::Event),
    RequestQuit,
    ConfirmQuit,
    CancelQuit,
    Escape(window::Id),
    DismissPanel,
    /// Tab and shift-tab: iced leaves the key to the application, so the fields
    /// of a form are only linked once we move the focus ourselves.
    FocusNext,
    FocusPrevious,
    PreferenceModelChanged(ModelChoice),
    PreferenceDefaultReasoningChanged(ReasoningMode),
    PreferenceGenerationModelChanged(ModelChoice),
    PreferenceGenerationReasoningChanged(ReasoningMode),
    PreferenceAccelerationModelChanged(ModelChoice),
    PreferencePermissionModeChanged(PermissionMode),
    PreferenceLegacyMtpChanged(bool),
    PreferenceDsparkChanged(bool),
    PreferenceTimeoutChanged(String),
    PreferenceA2uiChanged(bool),
    PreferenceEndpointPortChanged(String),
    PreferenceEndpointEnabledChanged(bool),
    PreferenceEndpointCorsChanged(bool),
    PreferenceDevBrainEnabledChanged(bool),
    PreferenceDevBrainVaultChanged(String),
    PreferenceGitDiffLayoutChanged(GitDiffLayout),
    PreferenceGitDiffAlgorithmChanged(GitDiffAlgorithm),
    PreferenceGitContextLinesChanged(String),
    PreferenceGitInterhunkLinesChanged(String),
    PreferenceGitIndentHeuristicChanged(bool),
    PreferenceGitWhitespaceChanged(GitDiffWhitespace),
    PreferenceGitIgnoreBlankLinesChanged(bool),
    ChooseDevBrainVault,
    DevBrainVaultPicked(Option<PathBuf>),
    RestoreDevBrainDefaultGuides,
    ConfirmRestoreDevBrainDefaultGuides,
    CancelRestoreDevBrainDefaultGuides,
    PreferenceContextChanged(String),
    PreferenceMaxTokensChanged(String),
    PreferenceSystemPromptAction(text_editor::Action),
    PreferenceTemperatureChanged(String),
    PreferenceTopPChanged(String),
    PreferenceMinPChanged(String),
    PreferenceSeedChanged(String),
    PreferenceCpuThreadsChanged(String),
    PreferencePowerChanged(String),
    PreferencePrefillChunkChanged(String),
    PreferenceQualityChanged(bool),
    PreferenceWarmWeightsChanged(bool),
    PreferenceMtpDraftChanged(String),
    PreferenceMtpMarginChanged(String),
    PreferenceGlmMtpChanged(bool),
    PreferenceGlmMtpTimingChanged(bool),
    PreferenceDsparkConfidenceChanged(String),
    PreferenceDsparkStrictChanged(bool),
    PreferenceDsparkExactSamplingChanged(bool),
    PreferenceSsdChanged(bool),
    PreferenceSsdColdChanged(bool),
    PreferenceSsdCacheChanged(String),
    PreferenceSsdFullLayersChanged(String),
    PreferenceSsdPreloadChanged(String),
    PreferenceSteeringFileChanged(String),
    PreferenceSteeringFfnChanged(String),
    PreferenceSteeringAttnChanged(String),
    PreferenceSimulatedMemoryChanged(String),
    PreferenceExpertProfileChanged(String),
    PreferenceKvBudgetChanged(String),
    PreferenceKvMinTokensChanged(String),
    PreferenceKvColdMaxChanged(String),
    PreferenceKvContinuedIntervalChanged(String),
    A2uiDataChanged(String, String, serde_json::Value),
    A2uiEditorAction(String, String, String, text_editor::Action),
    A2uiChoiceFilterChanged(String, String, String, String),
    A2uiAction(String, String, Option<String>),
    A2uiSelectTab(String, String, usize),
    A2uiToggleModal(String, String),
    A2uiImageLoaded(Option<i32>, String, Result<Vec<u8>, String>),
    A2uiPlayMedia(String, String, bool),
    RequestA2uiDismiss(String),
    ConfirmA2uiDismiss,
    A2uiPreviousSurface,
    A2uiNextSurface,
    ResetPreferences,
    SavePreferences,
    DownloadArtifact(ManagedArtifactId),
    ValidateArtifact(ManagedArtifactId),
    DeleteArtifact(ManagedArtifactId),
    ConfirmDeleteArtifact,
    CancelDeleteArtifact,
    StopModelDownload,
    DownloadProgressTick,
    ComposerAction(text_editor::Action),
    TranscriptAction(usize, text_editor::Action),
    ToggleReasoning(usize),
    OpenLink(markdown::Uri),
    CopyToolText(String),
    OpenToolOutput(PathBuf),
    AllowToolOnce,
    DenyTool,
    SubmitPrompt,
    StopGeneration,
    GenerationTick,
    ChatScrolled(scrollable::Viewport),
    ChooseProjectFolder,
    ProjectFolderPicked(Option<PathBuf>),
    ProjectNameChanged(String),
    ConfirmProject,
    CancelProject,
    ToggleProject(i32),
    DeleteProject(i32),
    CreateSession(i32),
    DraftProjectChanged(i32),
    SwitchGitBranch(String),
    ReasoningModeChanged(ReasoningMode),
    PermissionModeChanged(PermissionMode),
    ToggleGitFile(PathBuf),
    OpenGitDiff(PathBuf),
    SetGitDiffLayout(GitDiffLayout),
    GitDiffScrolled(scrollable::Viewport),
    GitCommitMessageChanged(String),
    GitStageSelected,
    GitUnstageSelected,
    GitCommit,
    ConfirmGitCommitAll,
    GitFetch,
    GitPull,
    GitPush,
    GitOperationTick,
    DiscardSession(i32),
    SelectSession(i32, i32),
    RequestDeleteSession(i32),
    ConfirmDeleteSession,
    OpenSessionMenu(i32),
    StartRenameSession(i32),
    SessionTitleChanged(String),
    ConfirmRenameSession,
    RetitleSession(i32),
    CompactSession(i32),
    RebuildSessionContext(i32),
    SetSessionState(i32, SessionState),
    ToggleArchivedSessions(i32),
    ShowChat,
    ShowA2ui,
    ShowGit,
    ShowStats,
    ToggleSidebar,
    StartSidebarDrag,
    DragSidebar(f32),
    EndSidebarDrag,
    MetricsTick,
    DiscardCacheEntry(PathBuf),
    ClearTransientCache,
}

impl App {
    pub(crate) fn load(main_window: window::Id) -> Self {
        let config = match Config::load(&config_path()) {
            Ok(config) => config,
            Err(error) => return Self::failed(error, main_window),
        };
        let path = application_support_path().join("data.sqlite3");
        match Database::open(&path) {
            Ok(mut database) => match database.load_projects() {
                Ok(projects) => {
                    let preference_draft = PreferenceDraft::from_saved(&config);
                    // Before the counters are taken, so they measure what is
                    // actually reachable.
                    sweep_orphan_checkpoints(&kv_cache_path(), &projects);
                    #[cfg(target_os = "macos")]
                    crate::engine::sweep_transient_cache(&transient_cache_path());
                    let context_limit = config.active_generation().context_tokens.max(0) as u32;
                    let default_permission_mode = config.default_permission_mode;
                    // Reopen on the project we left, with a fresh draft chat.
                    let last_project = config
                        .interface
                        .last_project_id
                        .filter(|id| projects.iter().any(|item| item.project.id == *id));
                    let drafts = last_project
                        .map(|id| (id, projects::draft_title(&projects, id)))
                        .into_iter()
                        .collect();
                    let metrics =
                        Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
                    let metrics_snapshot = metrics.snapshot();
                    let git_diff_layout = config.git.diff_layout;
                    #[cfg(target_os = "macos")]
                    let (runtime_config, generation_service, endpoint, service_error) =
                        spawn_services(&config, Arc::clone(&metrics));
                    Self {
                        main_window,
                        preferences_window: None,
                        model_manager_window: None,
                        help_window: None,
                        pending_model_delete: None,
                        #[cfg(target_os = "macos")]
                        _native_menu: None,
                        #[cfg(target_os = "macos")]
                        native_edit_commands: crate::native_edit::command_queue(),
                        #[cfg(target_os = "macos")]
                        native_edit_availability: crate::native_edit::availability_state(),
                        database: Some(database),
                        projects,
                        config,
                        preference_draft,
                        preference_error: None,
                        restore_dev_brain_confirmation: false,
                        selected_project: last_project,
                        selected_session: None,
                        permission_mode: default_permission_mode,
                        drafts,
                        git_states: HashMap::new(),
                        git_worktrees: HashMap::new(),
                        git_selected_project: None,
                        git_selected_files: HashSet::new(),
                        git_commit_message: String::new(),
                        git_diff: None,
                        git_diff_layout,
                        git_commit_all_confirmation: false,
                        git_operation: None,
                        last_git_scan: Instant::now() - GIT_SCAN_INTERVAL,
                        #[cfg(target_os = "macos")]
                        background_chats: HashMap::new(),
                        session_menu: None,
                        pending_session_delete: None,
                        session_rename: None,
                        expanded_archives: HashSet::new(),
                        sidebar_drag: false,
                        choosing_folder: false,
                        pending_project_path: None,
                        project_name_input: String::new(),
                        model_download: ModelDownload::Idle,
                        composer: text_editor::Content::new(),
                        queued_inputs: VecDeque::new(),
                        conversation: Vec::new(),
                        chat_follow_tail: true,
                        active_turn: None,
                        a2ui: crate::a2ui::Store::default(),
                        a2ui_history: Vec::new(),
                        a2ui_history_index: None,
                        a2ui_tabs: HashMap::new(),
                        a2ui_modals: HashSet::new(),
                        a2ui_editors: HashMap::new(),
                        a2ui_markdown: HashMap::new(),
                        help_markdown: markdown::Content::parse(include_str!(
                            "../docs/USER_GUIDE.md"
                        )),
                        a2ui_choice_filters: HashMap::new(),
                        a2ui_images: HashMap::new(),
                        a2ui_image_requests: HashSet::new(),
                        a2ui_image_loading: false,
                        pending_a2ui_dismissal: None,
                        a2ui_auto_switch_pending: false,
                        generating: false,
                        context_used: 0,
                        context_limit,
                        tokens_per_second: None,
                        detail_tab: DetailTab::Chat,
                        metrics,
                        metrics_snapshot,
                        metrics_history: VecDeque::with_capacity(120),
                        kv_cache_report: KvCacheReport::default(),
                        last_cache_scan: Instant::now() - CACHE_SCAN_INTERVAL,
                        last_http_requests: 0,
                        last_ssd_selected_requests: 0,
                        last_ssd_requested_bytes: 0,
                        last_ssd_wait_ms: 0,
                        #[cfg(target_os = "macos")]
                        generation_service,
                        #[cfg(target_os = "macos")]
                        active_generation: None,
                        #[cfg(target_os = "macos")]
                        active_compaction: None,
                        #[cfg(target_os = "macos")]
                        active_tool_check: None,
                        #[cfg(target_os = "macos")]
                        agent_tools: None,
                        #[cfg(target_os = "macos")]
                        active_tools: None,
                        #[cfg(target_os = "macos")]
                        tool_cards: Vec::new(),
                        #[cfg(target_os = "macos")]
                        pending_tool_approval: None,
                        #[cfg(target_os = "macos")]
                        active_titling: None,
                        #[cfg(target_os = "macos")]
                        runtime_config,
                        #[cfg(target_os = "macos")]
                        _endpoint: endpoint,
                        error: {
                            #[cfg(target_os = "macos")]
                            {
                                service_error
                            }
                            #[cfg(not(target_os = "macos"))]
                            {
                                None
                            }
                        },
                        activity: None,
                        context_notice: None,
                        stop_requested: false,
                        system_prompt_seen_at: 0,
                        manual_compaction_queued: false,
                        #[cfg(target_os = "macos")]
                        skip_compaction_once: false,
                        quit_confirmation: false,
                    }
                }
                Err(error) => Self::failed(
                    format!("Could not open the project database: {error}"),
                    main_window,
                ),
            },
            Err(error) => Self::failed(
                format!("Could not open the project database: {error}"),
                main_window,
            ),
        }
    }

    fn failed(error: String, main_window: window::Id) -> Self {
        let config = Config::default();
        let preference_draft = PreferenceDraft::from_saved(&config);
        let context_limit = config.active_generation().context_tokens.max(0) as u32;
        let default_permission_mode = config.default_permission_mode;
        let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
        let metrics_snapshot = metrics.snapshot();
        let git_diff_layout = config.git.diff_layout;
        #[cfg(target_os = "macos")]
        let (runtime_config, generation_service, endpoint, service_error) =
            spawn_services(&config, Arc::clone(&metrics));
        #[cfg(target_os = "macos")]
        let startup_error = service_error;
        #[cfg(not(target_os = "macos"))]
        let startup_error = None::<String>;
        Self {
            main_window,
            preferences_window: None,
            model_manager_window: None,
            help_window: None,
            pending_model_delete: None,
            #[cfg(target_os = "macos")]
            _native_menu: None,
            #[cfg(target_os = "macos")]
            native_edit_commands: crate::native_edit::command_queue(),
            #[cfg(target_os = "macos")]
            native_edit_availability: crate::native_edit::availability_state(),
            database: None,
            projects: Vec::new(),
            config,
            preference_draft,
            preference_error: None,
            restore_dev_brain_confirmation: false,
            selected_project: None,
            selected_session: None,
            permission_mode: default_permission_mode,
            drafts: HashMap::new(),
            git_states: HashMap::new(),
            git_worktrees: HashMap::new(),
            git_selected_project: None,
            git_selected_files: HashSet::new(),
            git_commit_message: String::new(),
            git_diff: None,
            git_diff_layout,
            git_commit_all_confirmation: false,
            git_operation: None,
            last_git_scan: Instant::now() - GIT_SCAN_INTERVAL,
            #[cfg(target_os = "macos")]
            background_chats: HashMap::new(),
            session_menu: None,
            pending_session_delete: None,
            session_rename: None,
            expanded_archives: HashSet::new(),
            sidebar_drag: false,
            choosing_folder: false,
            pending_project_path: None,
            project_name_input: String::new(),
            model_download: ModelDownload::Idle,
            composer: text_editor::Content::new(),
            queued_inputs: VecDeque::new(),
            conversation: Vec::new(),
            chat_follow_tail: true,
            active_turn: None,
            a2ui: crate::a2ui::Store::default(),
            a2ui_history: Vec::new(),
            a2ui_history_index: None,
            a2ui_tabs: HashMap::new(),
            a2ui_modals: HashSet::new(),
            a2ui_editors: HashMap::new(),
            a2ui_markdown: HashMap::new(),
            help_markdown: markdown::Content::parse(include_str!("../docs/USER_GUIDE.md")),
            a2ui_choice_filters: HashMap::new(),
            a2ui_images: HashMap::new(),
            a2ui_image_requests: HashSet::new(),
            a2ui_image_loading: false,
            pending_a2ui_dismissal: None,
            a2ui_auto_switch_pending: false,
            generating: false,
            context_used: 0,
            context_limit,
            tokens_per_second: None,
            detail_tab: DetailTab::Chat,
            metrics,
            metrics_snapshot,
            metrics_history: VecDeque::with_capacity(120),
            kv_cache_report: KvCacheReport::default(),
            last_cache_scan: Instant::now() - CACHE_SCAN_INTERVAL,
            last_http_requests: 0,
            last_ssd_selected_requests: 0,
            last_ssd_requested_bytes: 0,
            last_ssd_wait_ms: 0,
            #[cfg(target_os = "macos")]
            generation_service,
            #[cfg(target_os = "macos")]
            active_generation: None,
            #[cfg(target_os = "macos")]
            active_compaction: None,
            #[cfg(target_os = "macos")]
            active_tool_check: None,
            #[cfg(target_os = "macos")]
            agent_tools: None,
            #[cfg(target_os = "macos")]
            active_tools: None,
            #[cfg(target_os = "macos")]
            tool_cards: Vec::new(),
            #[cfg(target_os = "macos")]
            pending_tool_approval: None,
            #[cfg(target_os = "macos")]
            active_titling: None,
            #[cfg(target_os = "macos")]
            runtime_config,
            #[cfg(target_os = "macos")]
            _endpoint: endpoint,
            error: Some(match startup_error {
                Some(service_error) => format!("{error} {service_error}"),
                None => error,
            }),
            activity: None,
            context_notice: None,
            stop_requested: false,
            system_prompt_seen_at: 0,
            manual_compaction_queued: false,
            #[cfg(target_os = "macos")]
            skip_compaction_once: false,
            quit_confirmation: false,
        }
    }

    #[cfg(target_os = "macos")]
    fn take_chat_snapshot(&mut self) -> ChatSnapshot {
        ChatSnapshot {
            selected_project: self.selected_project.take(),
            selected_session: self.selected_session.take(),
            permission_mode: self.permission_mode,
            composer: std::mem::take(&mut self.composer),
            queued_inputs: std::mem::take(&mut self.queued_inputs),
            conversation: std::mem::take(&mut self.conversation),
            chat_follow_tail: std::mem::replace(&mut self.chat_follow_tail, true),
            active_turn: self.active_turn.take(),
            a2ui: std::mem::take(&mut self.a2ui),
            a2ui_history: std::mem::take(&mut self.a2ui_history),
            a2ui_history_index: self.a2ui_history_index.take(),
            a2ui_tabs: std::mem::take(&mut self.a2ui_tabs),
            a2ui_modals: std::mem::take(&mut self.a2ui_modals),
            a2ui_editors: std::mem::take(&mut self.a2ui_editors),
            a2ui_markdown: std::mem::take(&mut self.a2ui_markdown),
            a2ui_choice_filters: std::mem::take(&mut self.a2ui_choice_filters),
            a2ui_images: std::mem::take(&mut self.a2ui_images),
            a2ui_image_requests: std::mem::take(&mut self.a2ui_image_requests),
            a2ui_image_loading: std::mem::take(&mut self.a2ui_image_loading),
            pending_a2ui_dismissal: self.pending_a2ui_dismissal.take(),
            a2ui_auto_switch_pending: std::mem::take(&mut self.a2ui_auto_switch_pending),
            generating: std::mem::take(&mut self.generating),
            context_used: std::mem::take(&mut self.context_used),
            context_limit: std::mem::replace(
                &mut self.context_limit,
                self.config.active_generation().context_tokens.max(0) as u32,
            ),
            tokens_per_second: self.tokens_per_second.take(),
            detail_tab: std::mem::take(&mut self.detail_tab),
            active_generation: self.active_generation.take(),
            active_compaction: self.active_compaction.take(),
            active_tool_check: self.active_tool_check.take(),
            agent_tools: self.agent_tools.take(),
            active_tools: self.active_tools.take(),
            tool_cards: std::mem::take(&mut self.tool_cards),
            pending_tool_approval: self.pending_tool_approval.take(),
            active_titling: self.active_titling.take(),
            error: self.error.take(),
            activity: self.activity.take(),
            context_notice: self.context_notice.take(),
            stop_requested: std::mem::take(&mut self.stop_requested),
            system_prompt_seen_at: std::mem::take(&mut self.system_prompt_seen_at),
            manual_compaction_queued: std::mem::take(&mut self.manual_compaction_queued),
            skip_compaction_once: std::mem::take(&mut self.skip_compaction_once),
        }
    }

    #[cfg(target_os = "macos")]
    fn restore_chat_snapshot(&mut self, snapshot: ChatSnapshot) {
        self.selected_project = snapshot.selected_project;
        self.selected_session = snapshot.selected_session;
        self.permission_mode = snapshot.permission_mode;
        self.composer = snapshot.composer;
        self.queued_inputs = snapshot.queued_inputs;
        self.conversation = snapshot.conversation;
        self.chat_follow_tail = snapshot.chat_follow_tail;
        self.active_turn = snapshot.active_turn;
        self.a2ui = snapshot.a2ui;
        self.a2ui_history = snapshot.a2ui_history;
        self.a2ui_history_index = snapshot.a2ui_history_index;
        self.a2ui_tabs = snapshot.a2ui_tabs;
        self.a2ui_modals = snapshot.a2ui_modals;
        self.a2ui_editors = snapshot.a2ui_editors;
        self.a2ui_markdown = snapshot.a2ui_markdown;
        self.a2ui_choice_filters = snapshot.a2ui_choice_filters;
        self.a2ui_images = snapshot.a2ui_images;
        self.a2ui_image_requests = snapshot.a2ui_image_requests;
        self.a2ui_image_loading = snapshot.a2ui_image_loading;
        self.pending_a2ui_dismissal = snapshot.pending_a2ui_dismissal;
        self.a2ui_auto_switch_pending = snapshot.a2ui_auto_switch_pending;
        self.generating = snapshot.generating;
        self.context_used = snapshot.context_used;
        self.context_limit = snapshot.context_limit;
        self.tokens_per_second = snapshot.tokens_per_second;
        self.detail_tab = snapshot.detail_tab;
        self.active_generation = snapshot.active_generation;
        self.active_compaction = snapshot.active_compaction;
        self.active_tool_check = snapshot.active_tool_check;
        self.agent_tools = snapshot.agent_tools;
        self.active_tools = snapshot.active_tools;
        self.tool_cards = snapshot.tool_cards;
        self.pending_tool_approval = snapshot.pending_tool_approval;
        self.active_titling = snapshot.active_titling;
        self.error = snapshot.error;
        self.activity = snapshot.activity;
        self.context_notice = snapshot.context_notice;
        self.stop_requested = snapshot.stop_requested;
        self.system_prompt_seen_at = snapshot.system_prompt_seen_at;
        self.manual_compaction_queued = snapshot.manual_compaction_queued;
        self.skip_compaction_once = snapshot.skip_compaction_once;
    }

    #[cfg(target_os = "macos")]
    pub(super) fn leave_current_chat(&mut self) {
        let snapshot = self.take_chat_snapshot();
        if let Some(session_id) = snapshot.selected_session
            && (snapshot.needs_poll() || snapshot.a2ui_image_loading)
        {
            self.background_chats.insert(session_id, snapshot);
        }
    }

    #[cfg(target_os = "macos")]
    fn restore_background_chat(&mut self, session_id: i32) -> bool {
        let Some(mut snapshot) = self.background_chats.remove(&session_id) else {
            return false;
        };
        snapshot.chat_follow_tail = true;
        self.restore_chat_snapshot(snapshot);
        true
    }

    #[cfg(target_os = "macos")]
    fn poll_background_chats(&mut self) {
        let session_ids = self
            .background_chats
            .iter()
            .filter_map(|(session_id, chat)| chat.needs_poll().then_some(*session_id))
            .collect::<Vec<_>>();
        for session_id in session_ids {
            let foreground = self.take_chat_snapshot();
            let Some(background) = self.background_chats.remove(&session_id) else {
                self.restore_chat_snapshot(foreground);
                continue;
            };
            self.restore_chat_snapshot(background);
            self.poll_titling();
            self.poll_generation();
            let background = self.take_chat_snapshot();
            self.background_chats.insert(session_id, background);
            self.restore_chat_snapshot(foreground);
        }
    }

    #[cfg(target_os = "macos")]
    pub(super) fn session_is_active(&self, session_id: i32) -> bool {
        (self.selected_session == Some(session_id) && self.generating)
            || self
                .background_chats
                .get(&session_id)
                .is_some_and(|chat| chat.generating)
    }

    #[cfg(not(target_os = "macos"))]
    pub(super) fn session_is_active(&self, _session_id: i32) -> bool {
        false
    }

    fn active_chat_count(&self) -> usize {
        #[cfg(target_os = "macos")]
        return active_chat_total(
            self.generating,
            self.background_chats.values().map(|chat| chat.generating),
        );
        #[cfg(not(target_os = "macos"))]
        active_chat_total(self.generating, std::iter::empty())
    }

    fn project_has_active_chat(&self, project_id: i32) -> bool {
        self.projects
            .iter()
            .find(|item| item.project.id == project_id)
            .is_some_and(|item| {
                item.sessions
                    .iter()
                    .any(|session| self.session_is_active(session.id))
            })
    }

    pub(crate) fn update(&mut self, message: Message) -> Task<Message> {
        let message = match self.update_preference_message(message) {
            Ok(message) => message,
            Err(task) => return task,
        };
        let message = match self.update_model_manager_message(message) {
            Ok(message) => message,
            Err(task) => return task,
        };
        match message {
            Message::Noop => {
                #[cfg(target_os = "macos")]
                self.sync_native_menu();
            }
            #[cfg(target_os = "macos")]
            Message::NativeEdit(command) => {
                crate::native_edit::queue_command(&self.native_edit_commands, command)
            }
            Message::OpenPreferences => return self.open_preferences(),
            Message::PreferencesOpened(id) => {
                if self.preferences_window == Some(id) {
                    return window::gain_focus(id);
                }
            }
            Message::ClosePreferences => {
                if let Some(id) = self.preferences_window {
                    self.preference_error = None;
                    self.restore_dev_brain_confirmation = false;
                    return window::close(id);
                }
            }
            Message::ScrollPreferences(section) => return scroll_preferences_to(section),
            Message::OpenHelp => return self.open_help(),
            Message::HelpOpened(id) => {
                if self.help_window == Some(id) {
                    return window::gain_focus(id);
                }
            }
            Message::NewChat => {
                if let Some(project_id) = self.selected_project {
                    self.create_session(project_id);
                    return focus_composer();
                }
            }
            Message::ExportChat => {
                let Some(title) = self.active_chat_title().map(str::to_owned) else {
                    return Task::none();
                };
                let file_name = format!("{}.md", export_file_stem(&title));
                let content = export_markdown(&title, self.config.model, &self.conversation);
                return Task::perform(
                    async move {
                        AsyncFileDialog::new()
                            .set_title("Export chat as Markdown")
                            .set_file_name(&file_name)
                            .add_filter("Markdown", &["md"])
                            .save_file()
                            .await
                            .map(|file| file.path().to_path_buf())
                    },
                    move |path| Message::ExportChatPicked(path, content),
                );
            }
            Message::ExportChatPicked(path, content) => {
                if let Some(path) = path
                    && let Err(error) = fs::write(&path, content)
                {
                    self.error = Some(format!("Could not export the chat: {error}"));
                }
            }
            Message::WindowOpened(id) => {
                if id == self.main_window {
                    self.refresh_git_state();
                }
                #[cfg(target_os = "macos")]
                if id == self.main_window && self._native_menu.is_none() {
                    match crate::native_menu::install() {
                        Ok(menu) => self._native_menu = Some(menu),
                        Err(error) => {
                            self.error =
                                Some(format!("Could not install the application menu: {error}"))
                        }
                    }
                }
                #[cfg(target_os = "macos")]
                self.sync_native_menu();
                if id == self.main_window && self.selected_project.is_some() {
                    return focus_composer();
                }
            }
            Message::WindowEvent(id, event) => match event {
                window::Event::Opened { position, size } if id == self.main_window => {
                    self.config.interface.window_size =
                        [size.width.round() as u32, size.height.round() as u32];
                    self.config.interface.window_position = position
                        .map(|position| [position.x.round() as i32, position.y.round() as i32]);
                }
                window::Event::Moved(position) if id == self.main_window => {
                    self.config.interface.window_position =
                        Some([position.x.round() as i32, position.y.round() as i32]);
                }
                window::Event::Resized(size) if id == self.main_window => {
                    self.config.interface.window_size =
                        [size.width.round() as u32, size.height.round() as u32];
                }
                window::Event::CloseRequested => {
                    if id == self.main_window {
                        return self.update(Message::RequestQuit);
                    }
                    return window::close(id);
                }
                window::Event::Closed => {
                    if self.model_manager_window == Some(id) {
                        self.model_manager_window = None;
                        self.pending_model_delete = None;
                    }
                    if self.preferences_window == Some(id) {
                        self.preferences_window = None;
                        self.preference_error = None;
                        self.restore_dev_brain_confirmation = false;
                    }
                    if self.help_window == Some(id) {
                        self.help_window = None;
                    }
                }
                _ => {}
            },
            Message::RequestQuit => {
                if self.active_chat_count() == 0 {
                    return self.quit();
                }
                self.quit_confirmation = true;
            }
            Message::ConfirmQuit => return self.quit(),
            Message::CancelQuit => self.quit_confirmation = false,
            Message::Escape(id) => {
                if self.preferences_window == Some(id) {
                    if self.restore_dev_brain_confirmation {
                        self.restore_dev_brain_confirmation = false;
                        return Task::none();
                    }
                    return self.update(Message::ClosePreferences);
                }
                if id == self.main_window {
                    return self.update(Message::DismissPanel);
                }
            }
            Message::DismissPanel => {
                if self.quit_confirmation {
                    self.quit_confirmation = false;
                } else if self.git_diff.is_some() {
                    self.git_diff = None;
                } else if self.git_commit_all_confirmation {
                    self.git_commit_all_confirmation = false;
                } else if self.pending_session_delete.is_some() {
                    self.pending_session_delete = None;
                } else if self.pending_a2ui_dismissal.is_some() {
                    self.pending_a2ui_dismissal = None;
                } else if self.session_rename.is_some() || self.session_menu.is_some() {
                    self.session_rename = None;
                    self.session_menu = None;
                } else if self.pending_project_path.is_some() {
                    self.pending_project_path = None;
                    self.project_name_input.clear();
                    self.error = None;
                }
            }
            Message::FocusNext => {
                return iced::widget::operation::focus_next().chain(reveal_focused());
            }
            Message::FocusPrevious => {
                return iced::widget::operation::focus_previous().chain(reveal_focused());
            }
            Message::ShowChat => {
                self.detail_tab = DetailTab::Chat;
                self.chat_follow_tail = true;
                return scroll_chat_to_end();
            }
            Message::ShowA2ui => self.detail_tab = DetailTab::A2ui,
            Message::ShowGit => {
                self.detail_tab = DetailTab::Git;
                if let Some(project_id) = self.selected_project {
                    self.refresh_git_state();
                    self.refresh_git_worktree(project_id);
                }
            }
            Message::ShowStats => {
                self.detail_tab = DetailTab::Stats;
                self.scan_kv_cache();
            }
            Message::DiscardCacheEntry(path) => self.discard_cache_entry(&path),
            Message::ClearTransientCache => self.clear_transient_cache(),
            // The sidebar starts at the window's left edge, so the cursor's x is
            // the width the user is asking for. Only the final width is stored.
            Message::StartSidebarDrag => self.sidebar_drag = true,
            Message::DragSidebar(x) => {
                if self.sidebar_drag {
                    self.config.interface.sidebar_width =
                        (x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
                }
            }
            Message::EndSidebarDrag => {
                if self.sidebar_drag {
                    self.sidebar_drag = false;
                    self.store_config();
                }
            }
            Message::ToggleSidebar => {
                self.config.interface.sidebar_collapsed = !self.config.interface.sidebar_collapsed;
                self.store_config();
                #[cfg(target_os = "macos")]
                self.sync_native_menu();
            }
            Message::MetricsTick => self.sample_metrics(),
            Message::ComposerAction(action) => self.composer.perform(action),
            Message::TranscriptAction(index, action) => {
                if !action.is_edit()
                    && let Some(message) = self.conversation.get_mut(index)
                {
                    message.transcript.perform(action);
                }
            }
            Message::A2uiDataChanged(surface_id, path, value) => {
                if self.a2ui_history_index.is_some() {
                    return Task::none();
                }
                return self.change_a2ui_data(surface_id, path, value);
            }
            Message::A2uiEditorAction(surface_id, component_id, path, action) => {
                if self.a2ui_history_index.is_some() {
                    return Task::none();
                }
                let key = (surface_id.clone(), component_id, path.clone());
                let Some(editor) = self.a2ui_editors.get_mut(&key) else {
                    return Task::none();
                };
                editor.perform(action);
                let value = serde_json::Value::String(editor.text());
                return self.change_a2ui_data(surface_id, path, value);
            }
            Message::A2uiChoiceFilterChanged(surface_id, component_id, context_path, value) => {
                self.a2ui_choice_filters
                    .insert((surface_id, component_id, context_path), value);
            }
            Message::A2uiAction(surface_id, component_id, context_path) => {
                if self.a2ui_history_index.is_some() {
                    return Task::none();
                }
                match self
                    .a2ui
                    .action(&surface_id, &component_id, context_path.as_deref())
                {
                    Ok(action) => {
                        self.composer = text_editor::Content::with_text(&format!(
                            "A2UI client event:\n{}\nA2UI client metadata:\n{}",
                            serde_json::to_string(&action).unwrap_or_default(),
                            self.a2ui.client_metadata()
                        ));
                        self.chat_follow_tail = true;
                        self.start_generation();
                        return scroll_chat_to_end();
                    }
                    Err(error) => self.error = Some(error),
                }
            }
            Message::A2uiSelectTab(surface_id, component_id, index) => {
                self.a2ui_tabs.insert((surface_id, component_id), index);
            }
            Message::A2uiToggleModal(surface_id, component_id) => {
                let key = (surface_id, component_id);
                if !self.a2ui_modals.remove(&key) {
                    self.a2ui_modals.insert(key);
                }
            }
            Message::A2uiImageLoaded(session_id, url, result) => {
                if session_id != self.selected_session {
                    #[cfg(target_os = "macos")]
                    {
                        let foreground = self.take_chat_snapshot();
                        if let Some(background) = session_id
                            .and_then(|session_id| self.background_chats.remove(&session_id))
                        {
                            self.restore_chat_snapshot(background);
                            let task =
                                self.update(Message::A2uiImageLoaded(session_id, url, result));
                            let background = self.take_chat_snapshot();
                            self.background_chats.insert(
                                session_id.expect("background chat has a session"),
                                background,
                            );
                            self.restore_chat_snapshot(foreground);
                            return task;
                        }
                        self.restore_chat_snapshot(foreground);
                    }
                    return Task::none();
                }
                self.a2ui_image_loading = false;
                match result {
                    Ok(bytes) => {
                        self.a2ui_images
                            .insert(url, iced::widget::image::Handle::from_bytes(bytes));
                    }
                    Err(error) => {
                        self.error = Some(format!("Could not load an A2UI image: {error}"))
                    }
                }
                return self.load_next_a2ui_image();
            }
            Message::A2uiPlayMedia(url, title, video) => {
                #[cfg(target_os = "macos")]
                if let Err(error) = crate::native_media::open(&url, &title, video) {
                    self.error = Some(format!("Could not play media: {error}"));
                }
                #[cfg(not(target_os = "macos"))]
                {
                    let _ = (title, video);
                    if let Err(error) = std::process::Command::new("open").arg(url).spawn() {
                        self.error = Some(format!("Could not open media: {error}"));
                    }
                }
            }
            Message::RequestA2uiDismiss(surface_id) => {
                if self.generating {
                    self.error = Some(
                        "Stop the active generation before dismissing its A2UI surface.".into(),
                    );
                } else if self.a2ui_history_index.is_none()
                    && self.a2ui.surface(&surface_id).is_some()
                {
                    self.pending_a2ui_dismissal = Some(surface_id);
                }
            }
            Message::ConfirmA2uiDismiss => {
                let Some(surface_id) = self.pending_a2ui_dismissal.clone() else {
                    return Task::none();
                };
                let Some(session_id) = self.selected_session else {
                    self.error = Some("The A2UI surface is not attached to a saved chat.".into());
                    return Task::none();
                };
                let Some(database) = &mut self.database else {
                    self.error = Some("The project database is unavailable.".into());
                    return Task::none();
                };
                match database.dismiss_a2ui_surface(session_id, &surface_id) {
                    Ok(message) => {
                        self.conversation.push(ChatMessage::from(message));
                        self.a2ui_history.push(self.a2ui.clone());
                        self.a2ui.clear();
                        self.a2ui_history_index = None;
                        self.pending_a2ui_dismissal = None;
                        self.a2ui_tabs.clear();
                        self.a2ui_modals.clear();
                        self.sync_a2ui_renderer_state();
                        self.error = None;
                    }
                    Err(error) => {
                        self.error = Some(format!("Could not dismiss A2UI surface: {error}"));
                    }
                }
            }
            Message::A2uiPreviousSurface => {
                if !self.a2ui_history.is_empty() {
                    self.a2ui_history_index = Some(
                        self.a2ui_history_index
                            .map_or(self.a2ui_history.len() - 1, |index| index.saturating_sub(1)),
                    );
                    self.a2ui_tabs.clear();
                    self.a2ui_modals.clear();
                    self.sync_a2ui_renderer_state();
                    return self.load_next_a2ui_image();
                }
            }
            Message::A2uiNextSurface => {
                if let Some(index) = self.a2ui_history_index {
                    self.a2ui_history_index =
                        (index + 1 < self.a2ui_history.len()).then_some(index + 1);
                    self.a2ui_tabs.clear();
                    self.a2ui_modals.clear();
                    self.sync_a2ui_renderer_state();
                    return self.load_next_a2ui_image();
                }
            }
            Message::ToggleReasoning(index) => {
                if let Some(message) = self.conversation.get_mut(index)
                    && message.reasoning.is_some()
                {
                    message.reasoning_open = !message.reasoning_open;
                }
            }
            Message::OpenLink(uri) => {
                if let Ok(url) = url::Url::parse(&uri)
                    && matches!(url.scheme(), "http" | "https")
                    && let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn()
                {
                    self.error = Some(format!("Could not open the link: {error}"));
                }
            }
            Message::CopyToolText(value) => return iced::clipboard::write(value),
            Message::OpenToolOutput(path) => {
                if let Err(error) = std::process::Command::new("open").arg(path).spawn() {
                    self.error = Some(format!("Could not open the tool output: {error}"));
                }
            }
            Message::AllowToolOnce => {
                #[cfg(target_os = "macos")]
                if let Some((_, decision)) = self.pending_tool_approval.take() {
                    let _ = decision.send(true);
                }
            }
            Message::DenyTool =>
            {
                #[cfg(target_os = "macos")]
                if let Some((_, decision)) = self.pending_tool_approval.take() {
                    let _ = decision.send(false);
                }
            }
            Message::SubmitPrompt => {
                self.chat_follow_tail = true;
                self.start_generation();
                return scroll_chat_to_end();
            }
            Message::StopGeneration => {
                self.stop_requested = true;
                self.activity = Some("Stopping…".into());
                #[cfg(target_os = "macos")]
                if let Some(active) = &self.active_generation {
                    active.cancel.store(true, Ordering::Relaxed);
                }
                #[cfg(target_os = "macos")]
                if let Some(active) = &self.active_tools {
                    active.cancel.store(true, Ordering::Relaxed);
                }
                #[cfg(target_os = "macos")]
                if let Some((_, decision)) = self.pending_tool_approval.take() {
                    let _ = decision.send(false);
                }
                #[cfg(target_os = "macos")]
                self.stop_agent_jobs();
                #[cfg(target_os = "macos")]
                if let Some(compaction) = &self.active_compaction {
                    compaction.active.cancel.store(true, Ordering::Relaxed);
                }
                #[cfg(target_os = "macos")]
                if let Some(check) = &self.active_tool_check {
                    check.active.cancel.store(true, Ordering::Relaxed);
                }
            }
            Message::GenerationTick => {
                #[cfg(target_os = "macos")]
                self.poll_titling();
                self.poll_generation();
                #[cfg(target_os = "macos")]
                self.poll_background_chats();
                let images = self.load_next_a2ui_image();
                return images;
            }
            Message::ChatScrolled(viewport) => {
                let offset = viewport.absolute_offset();
                let reversed = viewport.absolute_offset_reversed();
                let follow = chat_near_end(self.chat_follow_tail, offset.y, reversed.y);
                if follow != self.chat_follow_tail {
                    self.chat_follow_tail = follow;
                    return iced::widget::operation::scroll_to(chat_scroll_id(), reversed);
                }
            }
            Message::ChooseProjectFolder => {
                self.choosing_folder = true;
                return Task::perform(
                    async {
                        AsyncFileDialog::new()
                            .set_title("Choose a project folder")
                            .pick_folder()
                            .await
                            .map(|folder| folder.path().to_path_buf())
                    },
                    Message::ProjectFolderPicked,
                );
            }
            Message::ProjectFolderPicked(path) => {
                self.choosing_folder = false;
                if let Some(path) = path {
                    self.prepare_project(path);
                }
            }
            Message::ProjectNameChanged(value) => self.project_name_input = value,
            Message::ConfirmProject => self.create_project(),
            Message::CancelProject => {
                self.pending_project_path = None;
                self.project_name_input.clear();
                self.error = None;
            }
            // Sidebar-only: collapsing a project leaves the open chat alone.
            Message::ToggleProject(project_id) => {
                let Some(item) = self
                    .projects
                    .iter_mut()
                    .find(|item| item.project.id == project_id)
                else {
                    return Task::none();
                };
                let collapsed = !item.project.collapsed;
                item.project.collapsed = collapsed;
                if let Some(database) = &mut self.database
                    && let Err(error) = database.set_project_collapsed(project_id, collapsed)
                {
                    self.error = Some(error);
                }
            }
            Message::DeleteProject(project_id) => {
                if self.project_has_active_chat(project_id) {
                    self.error =
                        Some("Stop the active generation before deleting its project.".into());
                    return Task::none();
                }
                let checkpoint_ids = self
                    .projects
                    .iter()
                    .find(|item| item.project.id == project_id)
                    .map(|item| {
                        item.sessions
                            .iter()
                            .map(|session| session.id)
                            .collect::<Vec<_>>()
                    })
                    .unwrap_or_default();
                for session_id in &checkpoint_ids {
                    if let Err(error) =
                        discard_session_checkpoint_files(&kv_cache_path(), *session_id)
                    {
                        self.error = Some(error);
                        return Task::none();
                    }
                }
                self.finish_cache_change();
                if let Some(database) = &mut self.database {
                    match database.delete_project(project_id) {
                        Ok(()) => {
                            #[cfg(not(target_os = "macos"))]
                            let _ = checkpoint_ids;
                            #[cfg(target_os = "macos")]
                            for session_id in checkpoint_ids {
                                self.background_chats.remove(&session_id);
                            }
                            self.drafts.remove(&project_id);
                            self.git_states.remove(&project_id);
                            self.git_worktrees.remove(&project_id);
                            if self.git_selected_project == Some(project_id) {
                                self.git_selected_project = None;
                                self.git_selected_files.clear();
                            }
                            if self.config.interface.last_project_id == Some(project_id) {
                                self.config.interface.last_project_id = None;
                                let _ = self.config.save(&config_path());
                            }
                            if self.selected_project == Some(project_id) {
                                self.selected_project = None;
                                self.selected_session = None;
                                self.conversation.clear();
                                self.context_notice = None;
                                self.clear_a2ui();
                                self.composer = text_editor::Content::new();
                                self.queued_inputs.clear();
                                self.system_prompt_seen_at = 0;
                                self.context_used = 0;
                                self.tokens_per_second = None;
                            }
                            self.reload_projects();
                            self.invalidate_dev_brain_context();
                            self.finish_cache_change();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::CreateSession(project_id) => {
                self.create_session(project_id);
                if self.draft_selected(project_id) {
                    return focus_composer();
                }
            }
            Message::DraftProjectChanged(project_id) => self.move_draft_to_project(project_id),
            Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch),
            Message::ReasoningModeChanged(mode) => {
                if self.active_chat_count() > 0 {
                    self.error =
                        Some("Stop all active generations before changing thinking mode.".into());
                } else if !self
                    .config
                    .supported_reasoning_modes(self.config.model)
                    .contains(&mode)
                {
                    self.error = Some(
                        "Think Max requires a context window of at least 393216 tokens.".into(),
                    );
                } else {
                    let mut config = self.config.clone();
                    config
                        .model_profiles
                        .entry(config.model)
                        .or_default()
                        .reasoning_mode = mode;
                    match config.save(&config_path()) {
                        Ok(()) => {
                            self.config = config;
                            self.preference_draft.default_reasoning_mode = mode;
                            self.preference_draft
                                .load_generation(self.config.model, mode);
                            self.context_limit =
                                self.config.active_generation().context_tokens.max(0) as u32;
                            #[cfg(target_os = "macos")]
                            {
                                self.agent_tools = None;
                            }
                            self.error = None;
                            #[cfg(target_os = "macos")]
                            preferences::update_runtime_config(&self.runtime_config, &self.config);
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::PermissionModeChanged(mode) => {
                #[cfg(target_os = "macos")]
                if self.active_tools.is_some() {
                    self.error = Some(
                        "Wait for the current tool batch before changing permission mode.".into(),
                    );
                    return Task::none();
                }
                if let Some(session_id) = self.selected_session {
                    let Some(database) = &mut self.database else {
                        self.error = Some("The project database is unavailable.".into());
                        return Task::none();
                    };
                    match database.set_session_permission_mode(session_id, mode) {
                        Ok(()) => {
                            self.permission_mode = mode;
                            self.error = None;
                            self.reload_projects();
                        }
                        Err(error) => {
                            self.error = Some(format!("Could not save permission mode: {error}"));
                        }
                    }
                } else {
                    self.permission_mode = mode;
                    self.error = None;
                }
            }
            Message::ToggleGitFile(path) => self.toggle_git_file(path),
            Message::OpenGitDiff(path) => self.open_git_diff(&path),
            Message::SetGitDiffLayout(layout) => self.git_diff_layout = layout,
            Message::GitDiffScrolled(viewport) => {
                let offset = scrollable::AbsoluteOffset {
                    x: viewport.absolute_offset().x,
                    y: 0.0,
                };
                return Task::batch([
                    iced::widget::operation::scroll_to(git_diff_old_scroll_id(), offset),
                    iced::widget::operation::scroll_to(git_diff_new_scroll_id(), offset),
                ]);
            }
            Message::GitCommitMessageChanged(message) => self.git_commit_message = message,
            Message::GitStageSelected => self.stage_selected_git_files(),
            Message::GitUnstageSelected => self.unstage_selected_git_files(),
            Message::GitCommit => self.commit_git(),
            Message::ConfirmGitCommitAll => self.confirm_commit_all_git(),
            Message::GitFetch => self.start_git_remote("fetch"),
            Message::GitPull => self.start_git_remote("pull"),
            Message::GitPush => self.start_git_remote("push"),
            Message::GitOperationTick => self.poll_git_operation(),
            Message::DiscardSession(project_id) => self.discard_session(project_id),
            Message::OpenSessionMenu(session_id) => {
                self.session_rename = None;
                self.session_menu = if self.session_menu == Some(session_id) {
                    None
                } else {
                    Some(session_id)
                };
            }
            Message::StartRenameSession(session_id) => {
                let title = self
                    .projects
                    .iter()
                    .flat_map(|project| &project.sessions)
                    .find(|session| session.id == session_id)
                    .map(|session| session.title.clone())
                    .unwrap_or_default();
                self.session_menu = None;
                self.session_rename = Some((session_id, title));
            }
            Message::SessionTitleChanged(title) => {
                if let Some((_, draft)) = &mut self.session_rename {
                    *draft = title;
                }
            }
            Message::ConfirmRenameSession => {
                // Keep the dialog open on failure so the edit is not lost.
                let Some((session_id, title)) = self.session_rename.clone() else {
                    return Task::none();
                };
                if let Some(database) = &mut self.database {
                    match database.rename_session(session_id, &title) {
                        Ok(()) => {
                            self.session_rename = None;
                            self.error = None;
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::RetitleSession(session_id) => {
                self.session_menu = None;
                #[cfg(target_os = "macos")]
                self.retitle_session(session_id);
                #[cfg(not(target_os = "macos"))]
                {
                    let _ = session_id;
                    self.error = Some("Local Metal generation requires macOS.".into());
                }
            }
            Message::CompactSession(session_id) => {
                self.session_menu = None;
                if !self.can_compact_session(session_id) {
                    self.error =
                        Some("Open an idle session with new chat before compacting it.".into());
                } else {
                    #[cfg(target_os = "macos")]
                    if let Err(error) = self.start_compaction(
                        generation::PendingContinuation::None,
                        "manual compact action",
                    ) {
                        self.error = Some(error);
                    }
                    #[cfg(not(target_os = "macos"))]
                    {
                        self.error = Some("Local Metal generation requires macOS.".into());
                    }
                }
            }
            Message::RebuildSessionContext(session_id) => {
                self.session_menu = None;
                if self.session_is_active(session_id) {
                    self.error =
                        Some("Stop the active generation before rebuilding context.".into());
                } else {
                    match discard_session_checkpoint_files(&kv_cache_path(), session_id) {
                        Ok(_) => {
                            self.error = None;
                            self.finish_cache_change();
                        }
                        Err(error) => {
                            self.error = Some(error);
                        }
                    }
                }
            }
            Message::SetSessionState(session_id, state) => {
                self.session_menu = None;
                if state == SessionState::Archived {
                    if self.session_is_active(session_id) {
                        self.error =
                            Some("Stop the active generation before archiving its session.".into());
                        return Task::none();
                    }
                    if let Err(error) =
                        discard_session_checkpoint_files(&kv_cache_path(), session_id)
                    {
                        self.error = Some(error);
                        return Task::none();
                    }
                    self.finish_cache_change();
                }
                if let Some(database) = &mut self.database {
                    match database.set_session_state(session_id, state) {
                        Ok(()) => {
                            self.error = None;
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::ToggleArchivedSessions(project_id) => {
                if !self.expanded_archives.remove(&project_id) {
                    self.expanded_archives.insert(project_id);
                }
            }
            Message::SelectSession(project_id, session_id) => {
                if self.selected_session == Some(session_id) {
                    return Task::none();
                }
                #[cfg(target_os = "macos")]
                {
                    self.leave_current_chat();
                    if self.restore_background_chat(session_id) {
                        self.chat_follow_tail = true;
                        self.remember_project(project_id);
                        if let Some(database) = &mut self.database
                            && let Err(error) = database.touch_session(session_id)
                        {
                            self.error = Some(format!("Could not update the session: {error}"));
                        }
                        self.reload_projects();
                        return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);
                    }
                }
                let saved_context = self
                    .projects
                    .iter()
                    .flat_map(|project| &project.sessions)
                    .find(|session| session.id == session_id)
                    .map(|session| {
                        (
                            session.context_used,
                            session.context_limit,
                            session.last_tokens_per_second,
                            session.permission_mode(),
                        )
                    });
                let Some(database) = &mut self.database else {
                    return Task::none();
                };
                let loaded = database.load_messages(session_id).and_then(|messages| {
                    database
                        .load_a2ui_messages(session_id)
                        .map(|a2ui| (messages, a2ui))
                });
                match loaded {
                    Ok((messages, a2ui)) => {
                        if let Err(error) = database.touch_session(session_id) {
                            self.error = Some(format!("Could not update the session: {error}"));
                            return Task::none();
                        }
                        self.conversation = messages.into_iter().map(ChatMessage::from).collect();
                        self.chat_follow_tail = true;
                        generation::promote_legacy_turn_summaries(&mut self.conversation);
                        self.context_notice = None;
                        self.clear_a2ui();
                        let (history, active, errors) =
                            crate::a2ui::replay_epochs(a2ui.iter().map(|message| {
                                (
                                    message.id,
                                    message.message_id,
                                    message.dismissed,
                                    message.json.as_str(),
                                )
                            }));
                        self.a2ui_history = history;
                        self.a2ui = active;
                        let restore_error = (!errors.is_empty()).then(|| {
                            format!("Could not restore some A2UI history: {}", errors.join("; "))
                        });
                        self.sync_a2ui_renderer_state();
                        self.composer = text_editor::Content::new();
                        self.remember_project(project_id);
                        self.selected_session = Some(session_id);
                        self.system_prompt_seen_at = 0;
                        self.queued_inputs.clear();
                        let (used, limit, tokens_per_second, permission_mode) =
                            saved_context.unwrap_or((0, 0, None, PermissionMode::default()));
                        self.context_used = used.max(0) as u32;
                        self.context_limit = if limit > 0 {
                            limit as u32
                        } else {
                            self.config.active_generation().context_tokens.max(0) as u32
                        };
                        self.tokens_per_second = tokens_per_second;
                        self.permission_mode = permission_mode;
                        self.error = restore_error;
                        self.reload_projects();
                        return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);
                    }
                    Err(error) => {
                        self.error = Some(format!("Could not load the chat session: {error}"));
                    }
                }
            }
            Message::RequestDeleteSession(session_id) => {
                if self.session_is_active(session_id) {
                    self.error =
                        Some("Stop the active generation before deleting its session.".into());
                    return Task::none();
                }
                if self
                    .projects
                    .iter()
                    .flat_map(|project| &project.sessions)
                    .any(|session| session.id == session_id)
                {
                    self.pending_session_delete = Some(session_id);
                    self.session_menu = None;
                    self.session_rename = None;
                    self.error = None;
                }
            }
            Message::ConfirmDeleteSession => {
                let Some(session_id) = self.pending_session_delete.take() else {
                    return Task::none();
                };
                if self.session_is_active(session_id) {
                    self.error =
                        Some("Stop the active generation before deleting its session.".into());
                    return Task::none();
                }
                if let Err(error) = discard_session_checkpoint_files(&kv_cache_path(), session_id) {
                    self.error = Some(error);
                    return Task::none();
                }
                self.finish_cache_change();
                if let Some(database) = &mut self.database {
                    match database.delete_session(session_id) {
                        Ok(()) => {
                            #[cfg(target_os = "macos")]
                            self.background_chats.remove(&session_id);
                            if self.session_menu == Some(session_id) {
                                self.session_menu = None;
                            }
                            if self
                                .session_rename
                                .as_ref()
                                .is_some_and(|(id, _)| *id == session_id)
                            {
                                self.session_rename = None;
                            }
                            if self.selected_session == Some(session_id) {
                                self.selected_session = None;
                                self.conversation.clear();
                                self.context_notice = None;
                                self.clear_a2ui();
                                self.composer = text_editor::Content::new();
                                self.queued_inputs.clear();
                                self.system_prompt_seen_at = 0;
                                self.context_used = 0;
                                self.tokens_per_second = None;
                            }
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            _ => unreachable!("domain messages are dispatched before the main update"),
        }
        Task::none()
    }

    pub(crate) fn subscription(&self) -> Subscription<Message> {
        let mut subscriptions = vec![
            keyboard::listen().filter_map(|event| match event {
                keyboard::Event::KeyPressed { key, modifiers, .. } => shortcut(key, modifiers),
                _ => None,
            }),
            // A focused text field takes escape for itself to drop its own
            // focus, so a dialog would never see it through `on_key_press`.
            iced::event::listen_with(|event, _, id| {
                matches!(
                    &event,
                    iced::Event::Keyboard(keyboard::Event::KeyPressed {
                        key: keyboard::Key::Named(keyboard::key::Named::Escape),
                        ..
                    })
                )
                .then_some(Message::Escape(id))
            }),
            window::events().filter_map(|(id, event)| {
                matches!(
                    event,
                    window::Event::Opened { .. }
                        | window::Event::Moved(_)
                        | window::Event::Resized(_)
                        | window::Event::CloseRequested
                        | window::Event::Closed
                )
                .then_some(Message::WindowEvent(id, event))
            }),
        ];
        subscriptions
            .push(iced::time::every(METRICS_SAMPLE_INTERVAL).map(|_| Message::MetricsTick));
        #[cfg(target_os = "macos")]
        subscriptions.push(iced::time::every(Duration::from_millis(50)).map(|_| {
            match crate::native_menu::next_event() {
                Some(crate::native_menu::NativeMenuEvent::Preferences) => Message::OpenPreferences,
                Some(crate::native_menu::NativeMenuEvent::ModelManager) => {
                    Message::OpenModelManager
                }
                Some(crate::native_menu::NativeMenuEvent::NewChat) => Message::NewChat,
                Some(crate::native_menu::NativeMenuEvent::ExportChat) => Message::ExportChat,
                Some(crate::native_menu::NativeMenuEvent::ToggleSidebar) => Message::ToggleSidebar,
                Some(crate::native_menu::NativeMenuEvent::ShowChat) => Message::ShowChat,
                Some(crate::native_menu::NativeMenuEvent::ShowA2ui) => Message::ShowA2ui,
                Some(crate::native_menu::NativeMenuEvent::ShowGit) => Message::ShowGit,
                Some(crate::native_menu::NativeMenuEvent::ShowStats) => Message::ShowStats,
                Some(crate::native_menu::NativeMenuEvent::Help) => Message::OpenHelp,
                Some(crate::native_menu::NativeMenuEvent::Quit) => Message::RequestQuit,
                Some(crate::native_menu::NativeMenuEvent::Edit(command)) => {
                    Message::NativeEdit(command)
                }
                None => Message::Noop,
            }
        }));
        if matches!(self.model_download, ModelDownload::Active(_)) {
            subscriptions.push(
                iced::time::every(Duration::from_secs(1)).map(|_| Message::DownloadProgressTick),
            );
        }
        if self.git_operation.is_some() {
            subscriptions.push(
                iced::time::every(Duration::from_millis(100)).map(|_| Message::GitOperationTick),
            );
        }
        #[cfg(target_os = "macos")]
        let titling = self.active_titling.is_some();
        #[cfg(not(target_os = "macos"))]
        let titling = false;
        #[cfg(target_os = "macos")]
        let background_polling = self.background_chats.values().any(ChatSnapshot::needs_poll);
        #[cfg(not(target_os = "macos"))]
        let background_polling = false;
        if self.generating || titling || background_polling {
            subscriptions.push(
                iced::time::every(Duration::from_millis(50)).map(|_| Message::GenerationTick),
            );
        }
        // The cursor leaves the thin divider as soon as it moves, so the drag is
        // followed through raw window events instead of the widget.
        if self.sidebar_drag {
            subscriptions.push(iced::event::listen_with(|event, _, _| match event {
                iced::Event::Mouse(mouse::Event::CursorMoved { position }) => {
                    Some(Message::DragSidebar(position.x))
                }
                iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
                    Some(Message::EndSidebarDrag)
                }
                _ => None,
            }));
        }
        Subscription::batch(subscriptions)
    }

    pub(crate) fn title(&self, id: window::Id) -> String {
        if self.preferences_window == Some(id) {
            "Preferences — DS4Server".to_owned()
        } else if self.model_manager_window == Some(id) {
            "Model Manager — DS4Server".to_owned()
        } else if self.help_window == Some(id) {
            "Help — DS4Server".to_owned()
        } else {
            "DS4Server".to_owned()
        }
    }

    fn sample_metrics(&mut self) {
        let prefill_tokens_per_second = self.metrics.take_prefill_sample();
        let (kv_read_bytes, kv_write_bytes) = self.metrics.take_kv_io_sample();
        let snapshot = self.metrics.snapshot();
        let http_requests_per_second = snapshot
            .http_requests
            .saturating_sub(self.last_http_requests) as f32
            / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let kv_read_bytes_per_second = kv_read_bytes as f32 / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let kv_write_bytes_per_second =
            kv_write_bytes as f32 / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let sample_seconds = METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let ssd_requests_per_second = snapshot
            .ssd_selected_requests
            .saturating_sub(self.last_ssd_selected_requests)
            as f32
            / sample_seconds;
        let ssd_bytes_per_second = snapshot
            .ssd_requested_bytes
            .saturating_sub(self.last_ssd_requested_bytes)
            as f32
            / sample_seconds;
        let ssd_wait_ms_per_second =
            snapshot.ssd_wait_ms.saturating_sub(self.last_ssd_wait_ms) as f32 / sample_seconds;
        self.last_http_requests = snapshot.http_requests;
        self.last_ssd_selected_requests = snapshot.ssd_selected_requests;
        self.last_ssd_requested_bytes = snapshot.ssd_requested_bytes;
        self.last_ssd_wait_ms = snapshot.ssd_wait_ms;
        self.metrics_history.push_back(MetricsPoint {
            decode_tokens_per_second: if snapshot.phase == crate::metrics::RuntimePhase::Generating
            {
                snapshot.decode_tokens_per_second
            } else {
                0.0
            },
            prefill_tokens_per_second,
            http_requests_per_second,
            kv_read_bytes_per_second,
            kv_write_bytes_per_second,
            ssd_requests_per_second,
            ssd_bytes_per_second,
            ssd_wait_ms_per_second,
        });
        if self.metrics_history.len() > 120 {
            self.metrics_history.pop_front();
        }
        self.metrics_snapshot = snapshot;
        if self.detail_tab == DetailTab::Stats
            && self.last_cache_scan.elapsed() >= CACHE_SCAN_INTERVAL
        {
            self.scan_kv_cache();
        }
        if self.detail_tab == DetailTab::Git
            && self.git_operation.is_none()
            && self.last_git_scan.elapsed() >= GIT_SCAN_INTERVAL
            && let Some(project_id) = self.selected_project
        {
            self.refresh_git_state();
            self.refresh_git_worktree(project_id);
        }
    }

    /// Removes one cache file. Checkpoints are disposable: the next turn on that
    /// conversation prefills again.
    fn discard_cache_entry(&mut self, path: &Path) {
        if let Err(error) = fs::remove_file(path) {
            self.error = Some(format!("Could not delete the checkpoint: {error}"));
            return;
        }
        let _ = fs::remove_file(path.with_extension("meta"));
        self.finish_cache_change();
    }

    /// Empties the transient store only. Session checkpoints belong to their
    /// session and are removed with it.
    fn clear_transient_cache(&mut self) {
        let Ok(files) = fs::read_dir(transient_cache_path()) else {
            return;
        };
        for file in files.flatten() {
            if file.metadata().is_ok_and(|metadata| metadata.is_file())
                && let Err(error) = fs::remove_file(file.path())
            {
                self.error = Some(format!("Could not clear the transient cache: {error}"));
                break;
            }
        }
        self.finish_cache_change();
    }

    /// Writes the configuration file, surfacing a failure in the error banner.
    fn store_config(&mut self) {
        if let Err(error) = self.config.save(&config_path()) {
            self.error = Some(error);
        }
    }

    fn quit(&mut self) -> Task<Message> {
        if self.database.is_none() {
            return iced::exit();
        }
        match self.config.save(&config_path()) {
            Ok(()) => iced::exit(),
            Err(error) => {
                self.quit_confirmation = false;
                self.error = Some(error);
                Task::none()
            }
        }
    }

    fn finish_cache_change(&mut self) {
        self.metrics.rescan_cache(&kv_cache_path());
        self.metrics_snapshot = self.metrics.snapshot();
        self.scan_kv_cache();
    }

    fn scan_kv_cache(&mut self) {
        let budget = self.config.runtime.kv_cache.settings().budget_bytes;
        self.kv_cache_report = crate::metrics::kv_cache_report(&kv_cache_path(), budget);
        self.last_cache_scan = Instant::now();
    }

    fn active_chat_title(&self) -> Option<&str> {
        let project_id = self.selected_project?;
        if let Some(session_id) = self.selected_session {
            return self
                .projects
                .iter()
                .flat_map(|project| &project.sessions)
                .find(|session| session.id == session_id)
                .map(|session| session.title.as_str());
        }
        self.drafts.get(&project_id).map(String::as_str)
    }

    fn open_help(&mut self) -> Task<Message> {
        if let Some(id) = self.help_window {
            return window::gain_focus(id);
        }
        let (id, open) = window::open(window::Settings {
            size: Size::new(760.0, 640.0),
            min_size: Some(Size::new(560.0, 420.0)),
            icon: Some(app_icon()),
            ..Default::default()
        });
        self.help_window = Some(id);
        open.map(Message::HelpOpened)
    }

    #[cfg(target_os = "macos")]
    fn sync_native_menu(&self) {
        let Some(menu) = &self._native_menu else {
            return;
        };
        let edit = *self
            .native_edit_availability
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        menu.update(
            self.selected_project.is_some(),
            self.active_chat_title().is_some(),
            !self.config.interface.sidebar_collapsed,
            self.detail_tab,
            edit,
        );
    }
}

#[cfg(target_os = "macos")]
fn spawn_services(
    config: &Config,
    metrics: Arc<Metrics>,
) -> (
    Arc<RwLock<Config>>,
    Option<GenerationService>,
    Option<crate::server::ServerHandle>,
    Option<String>,
) {
    let runtime_config = Arc::new(RwLock::new(config.clone()));
    let generation = match GenerationService::spawn(Arc::clone(&metrics)) {
        Ok(generation) => generation,
        Err(error) => return (runtime_config, None, None, Some(error)),
    };
    let endpoint = config.endpoint.enabled.then(|| {
        crate::server::ServerHandle::spawn(
            generation.clone(),
            Arc::clone(&runtime_config),
            models_path(),
            transient_cache_path(),
            u16::try_from(config.endpoint.port).unwrap_or(4000),
            config.endpoint.cors,
            metrics,
        )
    });
    match endpoint {
        Some(Ok(endpoint)) => (runtime_config, Some(generation), Some(endpoint), None),
        Some(Err(error)) => (runtime_config, Some(generation), None, Some(error)),
        None => (runtime_config, Some(generation), None, None),
    }
}

impl Drop for App {
    fn drop(&mut self) {
        if let ModelDownload::Active(download) = &self.model_download {
            download.cancel.store(true, Ordering::Relaxed);
        }
        #[cfg(target_os = "macos")]
        if let Some(active) = &self.active_generation {
            active.cancel.store(true, Ordering::Relaxed);
        }
        #[cfg(target_os = "macos")]
        if let Some(active) = &self.active_tools {
            active.cancel.store(true, Ordering::Relaxed);
        }
        #[cfg(target_os = "macos")]
        if let Some((_, decision)) = self.pending_tool_approval.take() {
            let _ = decision.send(false);
        }
    }
}

fn shortcut(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Option<Message> {
    match key.as_ref() {
        keyboard::Key::Character(",") if modifiers.command() => Some(Message::OpenPreferences),
        keyboard::Key::Character("m") if modifiers.command() && modifiers.shift() => {
            Some(Message::OpenModelManager)
        }
        keyboard::Key::Character("b") if modifiers.command() => Some(Message::ToggleSidebar),
        keyboard::Key::Character("n") if modifiers.command() => Some(Message::NewChat),
        keyboard::Key::Character("q") if modifiers.command() => Some(Message::RequestQuit),
        keyboard::Key::Named(keyboard::key::Named::Tab) => Some(if modifiers.shift() {
            Message::FocusPrevious
        } else {
            Message::FocusNext
        }),
        _ => None,
    }
}

fn chat_needs_poll(generating: bool, titling: bool) -> bool {
    generating || titling
}

fn active_chat_total(foreground: bool, background: impl IntoIterator<Item = bool>) -> usize {
    usize::from(foreground) + background.into_iter().filter(|active| *active).count()
}

fn export_file_stem(title: &str) -> String {
    let stem = title
        .chars()
        .map(|character| match character {
            '/' | ':' | '\0' => '-',
            character => character,
        })
        .collect::<String>();
    let stem = stem.trim().trim_matches('.');
    if stem.is_empty() {
        "DS4Server Chat".to_owned()
    } else {
        stem.to_owned()
    }
}

fn write_block_quote(output: &mut String, text: &str) {
    use std::fmt::Write;

    for line in text.lines() {
        let _ = writeln!(output, "> {line}");
    }
}

fn write_code_fence(output: &mut String, language: &str, text: &str) {
    use std::fmt::Write;

    let length = text
        .lines()
        .map(|line| {
            line.trim_start()
                .chars()
                .take_while(|character| *character == '`')
                .count()
                + 1
        })
        .max()
        .unwrap_or(3)
        .max(3);
    let fence = "`".repeat(length);
    let _ = write!(output, "\n{fence}{language}\n{text}\n{fence}\n");
}

fn export_markdown(title: &str, model: ModelChoice, conversation: &[ChatMessage]) -> String {
    use std::fmt::Write;

    let mut output = format!("# {title}\n");
    let mut index = 0;
    while let Some(message) = conversation.get(index) {
        index += 1;
        if message.system {
            continue;
        }
        if message.compaction {
            let _ = write!(
                output,
                "\n> Context compacted: {}\n",
                message.content.trim()
            );
            continue;
        }
        let label = if message.user {
            "You"
        } else if message.tool {
            "Tool"
        } else {
            "DS4"
        };
        let _ = write!(output, "\n## {label}\n");
        if let Some(reasoning) = message.reasoning.as_deref().filter(|text| !text.is_empty()) {
            output.push('\n');
            write_block_quote(&mut output, reasoning);
        }
        let visible = crate::agent::visible_content(&message.content);
        let visible = crate::a2ui::transcript_fallback(visible);
        let _ = write!(output, "\n{}\n", visible.trim());

        if message.user || message.tool {
            continue;
        }
        let tool_result = conversation.get(index).filter(|message| message.tool);
        let cards = crate::agent::stored_tool_cards(
            model,
            &message.content,
            tool_result.map(|message| message.content.as_str()),
            &message.tool_approval_reasons,
        );
        if cards.is_empty() {
            continue;
        }
        index += usize::from(tool_result.is_some());
        for (tool_index, card) in cards.into_iter().enumerate() {
            if card.call.name == "bash"
                && let Some(command) = card
                    .call
                    .arguments
                    .get("command")
                    .and_then(|value| value.as_str())
            {
                if let Some(reason) = card.approval_reason {
                    output.push('\n');
                    write_block_quote(&mut output, &reason);
                }
                write_code_fence(&mut output, "bash", command.trim());
                if let Some(result) = card.result {
                    write_code_fence(&mut output, "text", &result);
                }
            } else if let Some(result) = card.result {
                let _ = write!(
                    output,
                    "\n## Tool\n\nTool result {} ({}):\n{result}\n",
                    tool_index + 1,
                    card.call.name
                );
            }
        }
    }
    output
}

pub(crate) fn application_support_path() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
        .join("Library")
        .join("Application Support")
        .join(APP_ID)
}

pub(crate) fn models_path() -> PathBuf {
    application_support_path().join("models")
}

pub(crate) fn browser_profile_path() -> PathBuf {
    application_support_path().join("browser")
}

/// The settings file, beside the project database.
pub(crate) fn config_path() -> PathBuf {
    application_support_path().join("config.yaml")
}

pub(super) fn chat_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("chat-transcript")
}

pub(super) fn git_diff_old_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("git-diff-old")
}

pub(super) fn git_diff_new_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("git-diff-new")
}

pub(super) fn composer_id() -> iced::widget::Id {
    iced::widget::Id::new("chat-composer")
}

pub(super) fn preferences_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("preferences-fields")
}

fn preferences_section_id(section: PreferenceSection) -> iced::widget::Id {
    iced::widget::Id::new(section.anchor())
}

/// Scrolls the preferences form so the field that just took focus is inside
/// the viewport. Iced moves focus without touching the scroll offset, so a tab
/// past the fold would otherwise send the typing to a field nobody can see.
///
/// The focus callback carries no bounds, so the position is taken from the
/// innermost container around the focused widget — the row that holds the
/// label and its input.
fn reveal_focused() -> Task<Message> {
    reveal_preferences(None)
}

fn scroll_preferences_to(section: PreferenceSection) -> Task<Message> {
    reveal_preferences(Some(preferences_section_id(section)))
}

fn reveal_preferences(target: Option<iced::widget::Id>) -> Task<Message> {
    use iced::advanced::widget::{Id, Operation, operation};
    use iced::{Rectangle, Vector};

    struct Locate {
        target: Option<Id>,
        rows: Vec<Rectangle>,
        next_container: Option<Rectangle>,
        found: Option<Rectangle>,
    }

    impl<T> Operation<T> for Locate {
        fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
            let container = self.next_container.take();
            if let Some(bounds) = container {
                self.rows.push(bounds);
            }
            operate(self);
            if container.is_some() {
                self.rows.pop();
            }
        }

        fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
            if self
                .target
                .as_ref()
                .is_some_and(|target| id == Some(target))
            {
                self.found = Some(bounds);
            }
            self.next_container = Some(bounds);
        }

        fn focusable(
            &mut self,
            _id: Option<&Id>,
            bounds: Rectangle,
            state: &mut dyn operation::Focusable,
        ) {
            if self.target.is_none() && state.is_focused() {
                self.found = self.rows.last().copied().or(Some(bounds));
            }
        }

        fn finish(&self) -> operation::Outcome<T> {
            self.found.map_or(operation::Outcome::None, |field| {
                operation::Outcome::Chain(Box::new(Reveal {
                    field,
                    align_top: self.target.is_some(),
                }))
            })
        }
    }

    struct Reveal {
        field: Rectangle,
        align_top: bool,
    }

    impl<T> Operation<T> for Reveal {
        fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
            operate(self);
        }

        fn scrollable(
            &mut self,
            id: Option<&Id>,
            bounds: Rectangle,
            content_bounds: Rectangle,
            translation: Vector,
            state: &mut dyn operation::Scrollable,
        ) {
            if id != Some(&preferences_scroll_id()) {
                return;
            }
            let offset = if self.align_top {
                section_offset(self.field, bounds, translation.y)
            } else {
                let Some(offset) = reveal_offset(self.field, bounds, translation.y) else {
                    return;
                };
                offset
            };
            state.scroll_by(
                scrollable::AbsoluteOffset { x: 0.0, y: offset },
                bounds,
                content_bounds,
            );
        }
    }

    iced::advanced::widget::operate(Locate {
        target,
        rows: Vec::new(),
        next_container: None,
        found: None,
    })
}

fn section_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f32) -> f32 {
    field.y - scrolled - viewport.y
}

/// How far the scroll area has to move for `field` to sit fully inside
/// `viewport`, or `None` when it already does. A field taller than the viewport
/// lines up with its top edge.
///
/// Layout places the content in its own unscrolled coordinates, so `field` only
/// says where it sits once `scrolled` — the offset the area is already at — is
/// taken off it.
fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f32) -> Option<f32> {
    // Keep a row's worth of margin, so a revealed field never sits flush
    // against the edge of the viewport.
    const MARGIN: f32 = 12.0;
    let top = field.y - scrolled;
    let above = top - MARGIN - viewport.y;
    let below = top + field.height + MARGIN - (viewport.y + viewport.height);
    if above < 0.0 {
        Some(above)
    } else if below > 0.0 {
        Some(below)
    } else {
        None
    }
}

fn focus_composer() -> Task<Message> {
    iced::widget::operation::focus(composer_id())
}

fn scroll_chat_to_end() -> Task<Message> {
    iced::widget::operation::scroll_to(
        chat_scroll_id(),
        scrollable::AbsoluteOffset::<f32>::default(),
    )
}

fn chat_near_end(following: bool, offset: f32, reversed_offset: f32) -> bool {
    const TAIL_THRESHOLD: f32 = 24.0;
    if following {
        offset <= TAIL_THRESHOLD
    } else {
        reversed_offset <= TAIL_THRESHOLD
    }
}

/// Deletes session checkpoints whose session is gone. Deleting a session or a
/// project removes its checkpoint directly; this catches the cases that path
/// misses — an interrupted or failed delete, or files left by an older
/// database — so no unreachable checkpoint keeps occupying disc.
fn sweep_orphan_checkpoints(directory: &Path, projects: &[ProjectWithSessions]) -> bool {
    let known = projects
        .iter()
        .flat_map(|item| item.sessions.iter().map(|session| session.id))
        .collect::<HashSet<_>>();
    let Ok(files) = fs::read_dir(directory) else {
        return false;
    };
    let mut removed = false;
    for file in files.flatten() {
        let path = file.path();
        if path
            .extension()
            .is_some_and(|value| value == "compacting" || value == "tmp")
        {
            removed |= fs::remove_file(&path).is_ok();
            continue;
        }
        if path.extension().is_none_or(|value| value != "bin") {
            continue;
        }
        // Only numbered session checkpoints are ours to judge; anything else in
        // the directory is left alone.
        let orphan = path
            .file_stem()
            .and_then(|value| value.to_str())
            .and_then(|value| value.parse::<i32>().ok())
            .is_some_and(|id| !known.contains(&id));
        if orphan {
            removed |= fs::remove_file(&path).is_ok();
        }
    }
    removed
}

fn kv_cache_path() -> PathBuf {
    application_support_path().join("kv-cache")
}

fn session_checkpoint_path(session_id: i32) -> PathBuf {
    kv_cache_path().join(format!("{session_id}.bin"))
}

fn discard_session_checkpoint_files(directory: &Path, session_id: i32) -> Result<bool, String> {
    let mut removed = false;
    for extension in ["bin", "tmp", "compacting"] {
        let path = directory.join(format!("{session_id}.{extension}"));
        match fs::remove_file(&path) {
            Ok(()) => removed = true,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => {
                return Err(format!(
                    "Could not discard session checkpoint {}: {error}",
                    path.display()
                ));
            }
        }
    }
    Ok(removed)
}

fn session_compaction_checkpoint_path(session_id: i32) -> PathBuf {
    kv_cache_path().join(format!("{session_id}.compacting"))
}

/// Content-addressed KV cache for turns that belong to no stored session: the
/// HTTP endpoint and the app's own one-shot requests share it.
fn transient_cache_path() -> PathBuf {
    kv_cache_path().join("http")
}

pub(crate) fn app_icon() -> window::Icon {
    let decoder = png::Decoder::new(std::io::Cursor::new(include_bytes!(
        "../assets/app-icon.png"
    )));
    let mut reader = decoder
        .read_info()
        .expect("bundled application icon must be valid PNG");
    let mut rgba = vec![0; reader.output_buffer_size()];
    let info = reader
        .next_frame(&mut rgba)
        .expect("bundled application icon must decode");
    assert_eq!(info.color_type, png::ColorType::Rgba);
    assert_eq!(info.bit_depth, png::BitDepth::Eight);
    rgba.truncate(info.buffer_size());
    window::icon::from_rgba(rgba, info.width, info.height)
        .expect("bundled application icon dimensions must be valid")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn preferences_shortcut_and_dspark_support_are_explicit() {
        let message = shortcut(
            keyboard::Key::Character(",".into()),
            keyboard::Modifiers::COMMAND,
        );
        assert!(matches!(message, Some(Message::OpenPreferences)));
        let message = shortcut(
            keyboard::Key::Character("m".into()),
            keyboard::Modifiers::COMMAND | keyboard::Modifiers::SHIFT,
        );
        assert!(matches!(message, Some(Message::OpenModelManager)));
        assert!(matches!(
            shortcut(
                keyboard::Key::Character("q".into()),
                keyboard::Modifiers::COMMAND
            ),
            Some(Message::RequestQuit)
        ));
        let tab = keyboard::Key::Named(keyboard::key::Named::Tab);
        assert!(matches!(
            shortcut(tab.clone(), keyboard::Modifiers::empty()),
            Some(Message::FocusNext)
        ));
        assert!(matches!(
            shortcut(tab, keyboard::Modifiers::SHIFT),
            Some(Message::FocusPrevious)
        ));
        assert!(ModelChoice::DeepSeekV4Flash.supports_dspark());
        assert!(ModelChoice::DeepSeekV4Flash0731.supports_dspark());
        assert!(!ModelChoice::DeepSeekV4Pro.supports_dspark());
        assert!(!ModelChoice::Glm52.supports_dspark());
    }

    #[test]
    fn background_activity_drives_polling_indicators_and_quit_confirmation() {
        assert!(chat_needs_poll(true, false));
        assert!(chat_needs_poll(false, true));
        assert!(!chat_needs_poll(false, false));
        assert_eq!(active_chat_total(false, [true, false, true]), 2);
        assert_eq!(active_chat_total(true, [true, false, true]), 3);
    }

    #[test]
    fn tab_scrolls_a_field_back_into_the_preferences_viewport() {
        let viewport = iced::Rectangle {
            x: 0.0,
            y: 100.0,
            width: 700.0,
            height: 400.0,
        };
        let field = |y| iced::Rectangle {
            x: 0.0,
            y,
            width: 700.0,
            height: 30.0,
        };
        assert_eq!(reveal_offset(field(200.0), viewport, 0.0), None);
        // Above the fold: scroll back by the gap plus the margin.
        assert_eq!(reveal_offset(field(80.0), viewport, 0.0), Some(-32.0));
        // Below it: the bottom edge plus the margin comes into view.
        assert_eq!(reveal_offset(field(490.0), viewport, 0.0), Some(32.0));
        // A field the scroll already brought into view stays put, and one
        // further down moves by the gap alone, not by the whole scroll.
        assert_eq!(reveal_offset(field(690.0), viewport, 500.0), None);
        assert_eq!(reveal_offset(field(990.0), viewport, 500.0), Some(32.0));
        assert_eq!(section_offset(field(990.0), viewport, 500.0), 390.0);
    }

    #[test]
    fn chat_only_follows_updates_while_near_the_tail() {
        assert!(chat_near_end(true, 0.0, 500.0));
        assert!(!chat_near_end(true, 25.0, 475.0));
        assert!(chat_near_end(false, 500.0, 24.0));
        assert!(!chat_near_end(false, 400.0, 100.0));
    }

    #[test]
    fn ds4_gib_and_streaming_cache_inputs_are_typed() {
        assert_eq!(
            parse_streaming_cache("128").unwrap(),
            Some(StreamingCacheBudget::Experts(128))
        );
        assert_eq!(
            parse_streaming_cache("64gB").unwrap(),
            Some(StreamingCacheBudget::Gib(64))
        );
        assert!(parse_streaming_cache("1.5GB").is_err());
        assert_eq!(parse_optional_gib("Memory", "8GB").unwrap(), Some(8));
    }

    #[test]
    fn one_shot_titles_are_reduced_to_a_single_sidebar_line() {
        use generation::session_title;
        assert_eq!(
            session_title("\"Metal kernel debugging\"").as_deref(),
            Some("Metal kernel debugging")
        );
        assert_eq!(
            session_title("Fixing the tokenizer.\nThat is my answer.").as_deref(),
            Some("Fixing the tokenizer")
        );
        assert_eq!(session_title("   \n  \n").as_deref(), None);
        let long = "word ".repeat(40);
        let title = session_title(&long).expect("a long reply still yields a title");
        assert!(title.chars().count() <= 61, "{title}");
        assert!(title.ends_with('…'));
    }

    #[test]
    fn assistant_stream_keeps_reasoning_separate_from_the_answer() {
        let mut message = ChatMessage {
            id: 1,
            user: false,
            tool: false,
            system: false,
            compaction: false,
            compaction_tail_start: None,
            generation_stats: None,
            reasoning: Some(String::new()),
            reasoning_complete: false,
            reasoning_open: true,
            content: String::new(),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: markdown::Content::new(),
            transcript: text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        };
        message.append(true, "working it out");
        message.append(false, "**final answer**");
        message.refresh_markdown();
        assert_eq!(message.reasoning.as_deref(), Some("working it out"));
        assert!(message.reasoning_complete);
        assert_eq!(message.content, "**final answer**");
        assert!(!message.markdown.items().is_empty());
    }

    #[test]
    fn chat_export_formats_visible_turns_reasoning_and_bash_tools() {
        let message = |user, tool, system, content: &str| ChatMessage {
            id: 1,
            user,
            tool,
            system,
            compaction: false,
            compaction_tail_start: None,
            generation_stats: None,
            reasoning: None,
            reasoning_complete: true,
            reasoning_open: false,
            content: content.into(),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: markdown::Content::new(),
            transcript: text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        };
        let mut assistant = message(
            false,
            false,
            false,
            r#"I will check.
<｜DSML｜tool_calls>
<｜DSML｜invoke name="bash"><｜DSML｜parameter name="command" string="true">cat <<'EOF'
```
EOF</｜DSML｜parameter></｜DSML｜invoke>
</｜DSML｜tool_calls>"#,
        );
        assistant.reasoning = Some("First thought.\nSecond thought.".into());
        assistant.tool_approval_reasons = vec![Some("This only prints text.".into())];
        let exported = export_markdown(
            "A chat",
            ModelChoice::DeepSeekV4Flash,
            &[
                message(false, false, true, "private"),
                message(true, false, false, "hello"),
                assistant,
                message(false, true, false, "Tool result 1 (bash):\n```\none\ntwo\n"),
            ],
        );
        assert_eq!(
            exported,
            "# A chat\n\n## You\n\nhello\n\n## DS4\n\n> First thought.\n> Second thought.\n\nI will check.\n\n> This only prints text.\n\n````bash\ncat <<'EOF'\n```\nEOF\n````\n\n````text\n```\none\ntwo\n````\n"
        );
        assert!(!exported.contains("private"));
    }

    #[test]
    fn owned_runtime_paths_stay_in_application_support() {
        let root = application_support_path();
        assert!(models_path().starts_with(&root));
        assert!(browser_profile_path().starts_with(&root));
        assert!(config_path().starts_with(&root));
        assert!(kv_cache_path().starts_with(&root));
    }

    #[test]
    fn source_does_not_depend_on_a_sibling_ds4_checkout() {
        fn scan(path: &Path, forbidden: &str) {
            if path.is_dir() {
                for entry in fs::read_dir(path).unwrap() {
                    scan(&entry.unwrap().path(), forbidden);
                }
            } else if let Ok(source) = fs::read_to_string(path) {
                assert!(
                    !source.contains(forbidden),
                    "{} references a sibling DS4 checkout",
                    path.display()
                );
            }
        }

        let root = Path::new(env!("CARGO_MANIFEST_DIR"));
        let forbidden = format!("{}/", ["..", "ds4"].join("/"));
        for path in ["src", "native", "build.rs", "Cargo.toml", "Makefile"] {
            scan(&root.join(path), &forbidden);
        }
    }
}
