use super::*;

/// Asks for a title short enough to fit a sidebar row.
const TITLE_INSTRUCTION: &str = "Give this conversation a short title of at most six words. \
Reply with the title alone: no quotes, no trailing period, no explanation.";
const TITLE_MAX_TOKENS: i32 = 48;
const TITLE_MAX_CHARS: usize = 60;
const LEGACY_AGENTS_PREFIX: &str = "Project instructions from AGENTS.md:\n\n";

/// A one-shot generation that produces a session title. It runs against the
/// transient KV cache, so it never creates or touches a stored session.
#[cfg(target_os = "macos")]
pub(super) struct TitleRequest {
    session_id: i32,
    /// Title the session had when the request was queued. If it changed since,
    /// somebody renamed it by hand and that newer intent wins.
    expected: String,
    active: ActiveGeneration,
    content: String,
}

#[cfg(target_os = "macos")]
pub(super) enum PendingContinuation {
    None,
    User(String),
    Tool(crate::agent::ToolRunResult),
    DurableTool(Vec<PathBuf>),
}

#[cfg(target_os = "macos")]
pub(super) struct CompactionRequest {
    pub(super) active: ActiveGeneration,
    pending: PendingContinuation,
    message_ids: Vec<i32>,
}

#[cfg(target_os = "macos")]
#[derive(Clone, Copy)]
enum ToolCheckStage {
    ResultInitial,
    ResultAfterCompaction,
    BoundedResult,
    Instructions,
    InstructionsAfterCompaction,
}

#[cfg(target_os = "macos")]
pub(super) struct ToolResultCheck {
    pub(super) active: ActiveGeneration,
    result: crate::agent::ToolRunResult,
    stage: ToolCheckStage,
}

#[derive(Debug)]
pub(crate) struct ChatMessage {
    pub(super) id: i32,
    pub(super) user: bool,
    pub(super) tool: bool,
    pub(super) system: bool,
    pub(super) compaction: bool,
    pub(super) compaction_tail_start: Option<i32>,
    pub(super) generation_stats: Option<GenerationStats>,
    pub(super) reasoning: Option<String>,
    pub(super) reasoning_complete: bool,
    pub(super) reasoning_open: bool,
    pub(super) content: String,
    pub(super) model_content: Option<String>,
    pub(super) tool_approval_reasons: Vec<Option<String>>,
    pub(super) instruction_metadata: Option<String>,
    pub(super) markdown: markdown::Content,
    pub(super) transcript: text_editor::Content,
    pub(super) a2ui_lines_processed: usize,
    pub(super) a2ui_errors: Vec<String>,
    pub(super) a2ui_replies: Vec<serde_json::Value>,
    pub(super) a2ui_open_urls: Vec<String>,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct GenerationStats {
    pub(super) duration_ms: u64,
    pub(super) input_tokens: u32,
    pub(super) cached_tokens: u32,
    pub(super) output_tokens: u32,
}

impl GenerationStats {
    fn add(&mut self, other: Self) {
        self.duration_ms = self.duration_ms.saturating_add(other.duration_ms);
        self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
        self.cached_tokens = self.cached_tokens.saturating_add(other.cached_tokens);
        self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
    }
}

pub(super) struct TurnSummary {
    started_at: Instant,
    message_id: Option<i32>,
    stats: GenerationStats,
}

impl TurnSummary {
    fn new() -> Self {
        Self {
            started_at: Instant::now(),
            message_id: None,
            stats: GenerationStats::default(),
        }
    }

    fn record(&mut self, input_tokens: u32, cached_tokens: u32, output_tokens: u32) {
        self.stats.add(GenerationStats {
            duration_ms: 0,
            input_tokens,
            cached_tokens,
            output_tokens,
        });
    }

    fn finish(mut self) -> GenerationStats {
        self.stats.duration_ms =
            u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
        self.stats
    }
}

pub(super) fn promote_legacy_turn_summaries(messages: &mut [ChatMessage]) {
    let mut owner = None;
    for index in 0..messages.len() {
        if messages[index].user {
            owner = Some((index, messages[index].generation_stats.is_some()));
        } else if let Some(stats) = messages[index].generation_stats.take()
            && let Some((owner, false)) = owner
        {
            messages[owner]
                .generation_stats
                .get_or_insert_default()
                .add(stats);
        }
    }
}

const TOOL_PROTOCOL_CORRECTION: &str = "Tool protocol correction (no tool was executed): you emitted a complete tool call inside private reasoning. End reasoning, then emit the tool call again as assistant content using the required syntax; do not merely discuss it.";
const EMPTY_RESPONSE_CORRECTION: &str = "Protocol correction: your previous response was empty. Continue the task now with either a valid tool call or a final answer.";

impl ChatMessage {
    pub(super) fn append(&mut self, reasoning: bool, chunk: &str) {
        if reasoning {
            self.reasoning.get_or_insert_default().push_str(chunk);
        } else {
            self.reasoning_complete = true;
            self.content.push_str(chunk);
        }
    }

    pub(super) fn refresh_markdown(&mut self) {
        let visible = crate::agent::visible_content(&self.content);
        let visible = crate::a2ui::transcript_fallback(visible);
        let content = if self.reasoning.is_some() {
            visible.trim_start()
        } else {
            &visible
        };
        if self.transcript.text() != content {
            self.transcript = text_editor::Content::with_text(content);
        }
        if !self.user && !self.tool {
            self.markdown = markdown::Content::parse(content);
        }
    }
}

fn has_misplaced_tool_call(model: ModelChoice, message: &ChatMessage) -> bool {
    message.content.trim().is_empty()
        && message.reasoning.as_deref().is_some_and(|reasoning| {
            matches!(
                crate::agent::parse_tool_calls(model, reasoning),
                Ok((_, calls)) if !calls.is_empty()
            )
        })
}

fn is_empty_response(message: &ChatMessage) -> bool {
    message.content.trim().is_empty()
        && message
            .reasoning
            .as_deref()
            .is_none_or(|reasoning| reasoning.trim().is_empty())
}

fn correction_already_sent(conversation: &[ChatMessage], correction: &str) -> bool {
    conversation
        .iter()
        .rev()
        .skip(1)
        .take_while(|message| {
            !message.user && !(message.tool && message.content.starts_with("Tool result "))
        })
        .any(|message| message.tool && message.content == correction)
}

impl From<StoredMessage> for ChatMessage {
    fn from(message: StoredMessage) -> Self {
        let generation_stats = match (
            message.generation_duration_ms,
            message.input_tokens,
            message.cached_tokens,
            message.output_tokens,
        ) {
            (Some(duration_ms), Some(input_tokens), Some(cached_tokens), Some(output_tokens))
                if duration_ms >= 0
                    && input_tokens >= 0
                    && cached_tokens >= 0
                    && output_tokens >= 0 =>
            {
                Some(GenerationStats {
                    duration_ms: duration_ms as u64,
                    input_tokens: input_tokens as u32,
                    cached_tokens: cached_tokens as u32,
                    output_tokens: output_tokens as u32,
                })
            }
            _ => None,
        };
        let mut message = Self {
            id: message.id,
            user: message.user,
            tool: message.tool,
            system: message.system,
            compaction: message.compaction,
            compaction_tail_start: message.compaction_tail_start,
            generation_stats,
            reasoning: message.reasoning,
            reasoning_complete: message.reasoning_complete,
            reasoning_open: false,
            content: message.content,
            model_content: message.model_content,
            tool_approval_reasons: message
                .tool_approval_reasons
                .as_deref()
                .and_then(|reasons| serde_json::from_str(reasons).ok())
                .unwrap_or_default(),
            instruction_metadata: message.instruction_metadata,
            markdown: iced::widget::markdown::Content::new(),
            transcript: iced::widget::text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        };
        message.refresh_markdown();
        message
    }
}

fn sync_a2ui_message(
    store: &mut crate::a2ui::Store,
    database: &mut Option<Database>,
    session_id: Option<i32>,
    message: &mut ChatMessage,
) -> bool {
    let lines = crate::a2ui::extract_lines(&message.content);
    let mut renderable_surface_updated = false;
    for (index, line) in lines.iter().enumerate().skip(message.a2ui_lines_processed) {
        let applied = match &line.value {
            Ok(value) => store.apply(value.clone(), line.raw.clone(), message.id),
            Err(error) => Err(error.clone()),
        };
        match applied {
            Ok(applied) => {
                renderable_surface_updated |= line.value.as_ref().is_ok_and(|value| {
                    crate::a2ui::message_surface_id(value).is_some_and(|id| {
                        store.active_surface().is_some_and(|surface| {
                            surface.id == id && surface.components.contains_key("root")
                        })
                    })
                });
                if let Some(reply) = applied.reply {
                    message.a2ui_replies.push(reply);
                }
                if let Some(url) = applied.open_url {
                    message.a2ui_open_urls.push(url);
                }
                if let (Some(session_id), Some(database)) = (session_id, database.as_mut()) {
                    for raw in applied.raws {
                        if let Err(error) =
                            database.insert_a2ui_message(session_id, message.id, &raw)
                        {
                            message.a2ui_errors.push(format!(
                                "line {} could not be persisted: {error}",
                                index + 1
                            ));
                            break;
                        }
                    }
                }
            }
            Err(error) => message
                .a2ui_errors
                .push(format!("line {}: {error}", index + 1)),
        }
    }
    message.a2ui_lines_processed = lines.len();
    message.refresh_markdown();
    renderable_surface_updated
}

#[cfg(any(target_os = "macos", test))]
fn chat_turn(message: &ChatMessage) -> ChatTurn {
    ChatTurn {
        user: message.user,
        tool: message.tool,
        system: message.system,
        skip_previous_eos: false,
        reasoning: message.reasoning.clone(),
        reasoning_complete: message.reasoning_complete,
        content: message
            .model_content
            .clone()
            .unwrap_or_else(|| message.content.clone()),
    }
}

fn queued_prompt(messages: impl IntoIterator<Item = String>) -> Option<String> {
    let messages = messages.into_iter().collect::<Vec<_>>();
    match messages.as_slice() {
        [] => None,
        [message] => Some(message.clone()),
        _ => Some(
            messages
                .iter()
                .enumerate()
                .map(|(index, message)| format!("Queued user message {}:\n{message}", index + 1))
                .collect::<Vec<_>>()
                .join("\n\n"),
        ),
    }
}

fn compacted_context_start(messages: &[ChatMessage]) -> usize {
    let Some(marker_index) = messages.iter().rposition(|message| message.compaction) else {
        return 0;
    };
    messages[marker_index]
        .compaction_tail_start
        .and_then(|id| {
            messages[..marker_index]
                .iter()
                .position(|message| message.id == id)
        })
        .unwrap_or(marker_index + 1)
}

fn has_chat_after_last_compaction(messages: &[ChatMessage]) -> bool {
    let start = messages
        .iter()
        .rposition(|message| message.compaction)
        .map_or(0, |index| index + 1);
    messages[start..].iter().any(|message| {
        !message.system
            && !message.compaction
            && !message
                .content
                .starts_with(crate::agent::COMPACTION_OBSERVATION_PREFIX)
            && (!message.content.trim().is_empty()
                || message
                    .reasoning
                    .as_deref()
                    .is_some_and(|reasoning| !reasoning.trim().is_empty()))
    })
}

#[cfg(any(target_os = "macos", test))]
fn title_context(messages: impl IntoIterator<Item = ChatTurn>) -> Vec<ChatTurn> {
    let mut started = false;
    messages
        .into_iter()
        .filter(|message| {
            if !started {
                started = message.user && !message.tool && !message.system;
            }
            started && !message.system && !message.content.trim().is_empty()
        })
        .collect()
}

impl App {
    fn chat_system_prompt(&self, model: ModelChoice, prompt: &str) -> String {
        let mut prompt = crate::agent::system_prompt(model, prompt, self.config.dev_brain.enabled);
        if self.config.a2ui_enabled {
            prompt.push_str("\n\n");
            prompt.push_str(crate::a2ui::SYSTEM_PROMPT);
        }
        if let Some(skills) = crate::agent::agent_skills_prompt() {
            prompt.push_str("\n\n");
            prompt.push_str(&skills);
        }
        prompt
    }

    fn workspace_instruction_messages(
        &mut self,
        touched_paths: &[PathBuf],
        opening: bool,
    ) -> Vec<SystemMessage> {
        let Some(root) = self
            .projects
            .iter()
            .find(|project| Some(project.project.id) == self.selected_project)
            .map(|project| PathBuf::from(&project.project.path))
        else {
            return Vec::new();
        };
        let visible_start = compacted_context_start(&self.conversation);
        let reconciliation = crate::instructions::reconcile(
            &root,
            &application_support_path().join("AGENTS.md"),
            self.conversation
                .iter()
                .enumerate()
                .filter_map(|(index, message)| {
                    message.instruction_metadata.as_deref().map(|metadata| {
                        crate::instructions::HistoryEntry {
                            metadata,
                            visible: index >= visible_start,
                        }
                    })
                }),
            touched_paths,
            opening,
        );
        if let Some(diagnostic) = reconciliation.diagnostic {
            self.context_notice = Some(diagnostic);
        }
        reconciliation
            .messages
            .into_iter()
            .map(|message| SystemMessage::instruction(message.content, message.metadata))
            .collect()
    }

    fn dev_brain_skills_prompt(&self) -> Option<String> {
        self.config.dev_brain.enabled.then(|| {
            let projects = self
                .projects
                .iter()
                .map(|project| project.project.clone())
                .collect::<Vec<_>>();
            crate::dev_brain::skills_prompt(&self.config.dev_brain, &projects).unwrap_or_else(|error| {
                format!(
                    "# Available Dev Brain skills\n\nThe verified skill index is unavailable: {error}. Repair the vault with dev_brain_info and dev_brain_validate before relying on a skill."
                )
            })
        })
    }

    pub(super) fn can_compact_session(&self, session_id: i32) -> bool {
        !self.generating
            && self.selected_session == Some(session_id)
            && has_chat_after_last_compaction(&self.conversation)
    }

    pub(super) fn start_generation(&mut self) {
        if self.selected_project.is_none() {
            return;
        }
        let prompt = self.composer.text().trim().to_owned();
        if prompt.is_empty() {
            return;
        }
        #[cfg(target_os = "macos")]
        if prompt == "/compact" {
            self.composer = text_editor::Content::new();
            if self.generating {
                self.manual_compaction_queued = true;
                self.activity = Some("Compaction queued for the next safe point…".into());
            } else if let Err(error) =
                self.start_compaction(PendingContinuation::None, "manual /compact request")
            {
                self.error = Some(error);
            }
            return;
        }
        if self.generating {
            self.queued_inputs.push_back(prompt);
            self.composer = text_editor::Content::new();
            self.activity = Some(format!(
                "{} queued input{}",
                self.queued_inputs.len(),
                if self.queued_inputs.len() == 1 {
                    ""
                } else {
                    "s"
                }
            ));
            return;
        }
        #[cfg(target_os = "macos")]
        self.reset_agent_tool_repeats();
        #[cfg(target_os = "macos")]
        let opening_turn = self.selected_session.is_none();
        self.a2ui_auto_switch_pending = true;
        self.context_notice = None;
        #[cfg(target_os = "macos")]
        self.tool_cards.clear();
        #[cfg(target_os = "macos")]
        if !std::mem::take(&mut self.skip_compaction_once)
            && crate::compaction::should_compact(self.context_used, self.context_limit)
        {
            self.active_turn.get_or_insert_with(TurnSummary::new);
            if let Err(error) = self.start_compaction(
                PendingContinuation::User(prompt),
                "soft limit before user turn",
            ) {
                self.active_turn = None;
                self.error = Some(error);
            }
            return;
        }
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path());
        let mut effective = match effective {
            Ok(settings) => settings,
            Err(error) => {
                self.error = Some(error);
                return;
            }
        };
        effective.turn.system_prompt =
            self.chat_system_prompt(model, &effective.turn.system_prompt);
        effective.turn.system_prompt = crate::compaction::summary_system_prompt(
            &effective.turn.system_prompt,
            self.compaction_summary(),
        );
        #[cfg(target_os = "macos")]
        let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
        #[cfg(target_os = "macos")]
        let model_prompt = if self.config.a2ui_enabled {
            format!(
                "{prompt}\n\nA2UI client metadata:\n{}",
                self.a2ui.client_metadata()
            )
        } else {
            prompt.clone()
        };
        #[cfg(target_os = "macos")]
        let mut injected_system = self.workspace_instruction_messages(&[], opening_turn);
        #[cfg(target_os = "macos")]
        if opening_turn {
            injected_system.extend(self.dev_brain_skills_prompt().map(SystemMessage::plain));
            injected_system.push(SystemMessage::plain(crate::agent::datetime_context()));
        }
        #[cfg(target_os = "macos")]
        let reminder_injected = self.system_prompt_reminder_due();
        #[cfg(target_os = "macos")]
        if reminder_injected {
            injected_system.extend(
                self.system_prompt_reminders(model)
                    .into_iter()
                    .map(SystemMessage::plain),
            );
        }
        #[cfg(target_os = "macos")]
        let mut messages = self
            .model_chat_messages()
            .into_iter()
            .map(chat_turn)
            .collect::<Vec<_>>();
        #[cfg(target_os = "macos")]
        messages.extend(
            injected_system
                .iter()
                .filter(|message| !message.content.is_empty())
                .map(|message| ChatTurn {
                    user: false,
                    tool: false,
                    system: true,
                    skip_previous_eos: false,
                    reasoning: None,
                    reasoning_complete: true,
                    content: message.content.clone(),
                }),
        );
        #[cfg(target_os = "macos")]
        messages.push(ChatTurn {
            user: true,
            tool: false,
            system: false,
            skip_previous_eos: false,
            reasoning: None,
            reasoning_complete: true,
            content: model_prompt.clone(),
        });

        #[cfg(target_os = "macos")]
        {
            // A draft session only reaches the database once there is a turn to store.
            let session_id = match self.selected_session {
                Some(session_id) => session_id,
                None => {
                    let Some(project_id) = self.selected_project else {
                        return;
                    };
                    match self.persist_session(project_id) {
                        Ok(session_id) => session_id,
                        Err(error) => {
                            self.error = Some(format!("Could not create the session: {error}"));
                            return;
                        }
                    }
                }
            };
            let archived_session = self
                .projects
                .iter()
                .flat_map(|project| &project.sessions)
                .find(|session| session.id == session_id)
                .is_some_and(|session| session.state() == SessionState::Archived);
            let Some(service) = self.generation_service.clone() else {
                self.error = Some("The model runtime is unavailable.".into());
                return;
            };
            let Some(database) = &mut self.database else {
                return;
            };
            let mut saved = match database.start_chat_turn(
                session_id,
                &prompt,
                (model_prompt != prompt).then_some(model_prompt.as_str()),
                &injected_system,
                assistant_reasoning,
            ) {
                Ok(turn) => turn,
                Err(error) => {
                    self.error = Some(format!("Could not save the chat turn: {error}"));
                    return;
                }
            };
            if archived_session {
                self.reload_projects();
                self.context_notice = Some(
                    "Rebuilding context: the session was archived and its checkpoint was discarded."
                        .into(),
                );
            }
            let user_id = saved[saved.len() - 2].id;
            self.active_turn
                .get_or_insert_with(TurnSummary::new)
                .message_id = Some(user_id);
            if reminder_injected {
                self.system_prompt_seen_at = self.context_used;
            }
            let idle_timeout =
                Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
            let checkpoint = if opening_turn {
                CheckpointTarget::Local {
                    checkpoint: session_checkpoint_path(session_id),
                    bootstrap: Some(transient_cache_path()),
                }
            } else {
                CheckpointTarget::Local {
                    checkpoint: session_checkpoint_path(session_id),
                    bootstrap: None,
                }
            };
            self.active_generation = match service.generate(
                effective.engine,
                effective.turn,
                messages,
                checkpoint,
                WorkSource::LocalChat,
                idle_timeout,
            ) {
                Ok(active) => Some(active),
                Err(error) => {
                    self.generation_service = None;
                    self.error = Some(error);
                    self.finish_turn_summary();
                    return;
                }
            };
            let mut assistant = ChatMessage::from(saved.pop().unwrap());
            let user = ChatMessage::from(saved.pop().unwrap());
            for message in saved {
                self.conversation.push(ChatMessage::from(message));
            }
            assistant.reasoning_open = assistant_reasoning;
            self.composer = text_editor::Content::new();
            self.conversation.push(user);
            self.conversation.push(assistant);
            self.generating = true;
            self.stop_requested = false;
            self.activity = Some("Loading model…".into());
            self.tokens_per_second = None;
            self.error = None;
            if opening_turn {
                // Queued behind the reply, so the answer is never delayed by it.
                self.request_first_title(session_id);
            }
        }
        #[cfg(not(target_os = "macos"))]
        {
            let _ = effective;
            self.error = Some("Local Metal generation requires macOS.".into());
        }
    }

    fn system_prompt_reminder_due(&self) -> bool {
        crate::agent::prompt_reminder_due(self.context_used, self.system_prompt_seen_at)
    }

    fn system_prompt_reminders(&self, model: ModelChoice) -> Vec<String> {
        let mut reminders = vec![crate::agent::system_prompt_reminder(
            model,
            self.config.dev_brain.enabled,
        )];
        if let Some(skills) = self.dev_brain_skills_prompt() {
            reminders.push(skills);
        }
        reminders.extend(crate::agent::agent_skills_prompt());
        if self.config.a2ui_enabled {
            reminders.push(crate::a2ui::SYSTEM_PROMPT.to_owned());
        }
        if !self.config.system_prompt.trim().is_empty() {
            reminders.push(self.config.system_prompt.clone());
        }
        reminders
    }

    pub(super) fn poll_generation(&mut self) -> bool {
        let was_generating = self.generating;
        let changed = self.poll_generation_step();
        if !self.generating {
            self.finish_turn_summary();
        }
        if was_generating && !self.generating {
            self.refresh_git_state();
            if self.detail_tab == DetailTab::Git
                && let Some(project_id) = self.selected_project
            {
                self.refresh_git_worktree(project_id);
            }
        }
        changed
    }

    fn poll_generation_step(&mut self) -> bool {
        #[cfg(target_os = "macos")]
        if self.active_compaction.is_some() {
            return self.poll_compaction();
        }
        #[cfg(target_os = "macos")]
        if self.active_tool_check.is_some() {
            return self.poll_tool_result_check();
        }
        #[cfg(target_os = "macos")]
        if self.active_tools.is_some() {
            while let Some(event) = self
                .active_tools
                .as_ref()
                .and_then(crate::agent::try_tool_event)
            {
                match event {
                    crate::agent::ToolEvent::State {
                        index,
                        state,
                        result,
                    } => {
                        if let Some(card) = self.tool_cards.get_mut(index) {
                            card.state = state;
                            if result.is_some() {
                                card.result = result;
                            }
                        }
                        self.activity = Some(format!("Tool {} · {}", index + 1, state.label()));
                    }
                    crate::agent::ToolEvent::Approval {
                        index,
                        prompt,
                        decision,
                    } => {
                        if let Some(card) = self.tool_cards.get_mut(index) {
                            card.state = crate::agent::ToolLifecycle::AwaitingApproval;
                        }
                        self.pending_tool_approval = Some((prompt, decision));
                        self.activity = Some(format!("Tool {} · Awaiting approval", index + 1));
                    }
                    crate::agent::ToolEvent::ApprovalReason { index, reason } => {
                        if let Some(card) = self.tool_cards.get_mut(index) {
                            card.approval_reason = Some(reason);
                        }
                        let reasons = self
                            .tool_cards
                            .iter()
                            .map(|card| card.approval_reason.clone())
                            .collect::<Vec<_>>();
                        if let Some(message) = self.conversation.last_mut() {
                            message.tool_approval_reasons = reasons.clone();
                            if let Some(database) = &mut self.database
                                && let Err(error) = database
                                    .update_message_tool_approval_reasons(message.id, &reasons)
                            {
                                self.error =
                                    Some(format!("Could not save tool approval reasons: {error}"));
                            }
                        }
                    }
                }
            }
        }
        #[cfg(target_os = "macos")]
        if let Some(active) = &self.active_tools {
            match crate::agent::try_tool_result(active) {
                Ok(Some(result)) => {
                    let cancelled = active.cancel.load(Ordering::Relaxed);
                    self.active_tools = None;
                    self.pending_tool_approval = None;
                    if cancelled {
                        self.generating = false;
                        self.activity = Some("Stopped".into());
                        self.start_next_queued();
                        return false;
                    }
                    let continuation = if self.manual_compaction_queued {
                        self.manual_compaction_queued = false;
                        self.start_compaction(
                            PendingContinuation::Tool(result),
                            "queued manual compaction",
                        )
                    } else {
                        self.start_tool_result_check(result, ToolCheckStage::ResultInitial)
                    };
                    if let Err(error) = continuation {
                        self.generating = false;
                        self.activity = Some("Failed".into());
                        self.error = Some(error);
                    }
                    return true;
                }
                Ok(None) => return false,
                Err(error) => {
                    self.active_tools = None;
                    self.generating = false;
                    self.activity = Some("Failed".into());
                    self.error = Some(error);
                    return false;
                }
            }
        }
        #[cfg(target_os = "macos")]
        let Some(active) = &mut self.active_generation else {
            self.generating = false;
            return false;
        };
        #[cfg(target_os = "macos")]
        let mut transcript_changed = false;
        #[cfg(target_os = "macos")]
        let mut context_changed = false;
        #[cfg(target_os = "macos")]
        let mut start_queued = false;
        #[cfg(target_os = "macos")]
        let mut continuation_feedback = None;
        #[cfg(target_os = "macos")]
        let mut a2ui_changed = false;
        #[cfg(target_os = "macos")]
        loop {
            match active.events.try_recv() {
                Ok(GenerationEvent::Loading) => {}
                Ok(GenerationEvent::Activity(activity)) => {
                    if activity.starts_with("Rebuilding context:") {
                        self.context_notice = Some(activity.to_owned());
                    }
                    self.activity = Some(activity.into());
                }
                Ok(GenerationEvent::Compacted(_)) => {
                    self.generating = false;
                    self.error =
                        Some("The model runtime returned an unexpected compaction event.".into());
                }
                Ok(GenerationEvent::Chunk { reasoning, content }) => {
                    if let Some(message) = self.conversation.last_mut()
                        && !message.user
                    {
                        message.append(reasoning, &content);
                        if !reasoning
                            && self.tool_cards.is_empty()
                            && crate::agent::has_tool_markup(&message.content)
                        {
                            self.tool_cards.push(crate::agent::ToolCard::streaming());
                            self.activity = Some("Parsing tool call…".into());
                        }
                        transcript_changed = true;
                    }
                    if !reasoning
                        && self.config.a2ui_enabled
                        && let Some(message) = self.conversation.last_mut()
                    {
                        let renderable_surface_updated = sync_a2ui_message(
                            &mut self.a2ui,
                            &mut self.database,
                            self.selected_session,
                            message,
                        );
                        a2ui_changed = true;
                        if self.a2ui_auto_switch_pending && renderable_surface_updated {
                            self.detail_tab = DetailTab::A2ui;
                            self.a2ui_auto_switch_pending = false;
                        }
                    }
                }
                Ok(GenerationEvent::Context {
                    used,
                    limit,
                    tokens_per_second,
                }) => {
                    self.context_used = used;
                    self.context_limit = limit;
                    self.tokens_per_second = tokens_per_second;
                    if !self.stop_requested {
                        self.activity = Some(
                            if tokens_per_second.is_some() {
                                "Generating…"
                            } else {
                                "Reading conversation…"
                            }
                            .into(),
                        );
                    }
                    context_changed = true;
                }
                Ok(GenerationEvent::Finished(result)) => {
                    if let Ok(output) = &result
                        && let Some(turn) = &mut self.active_turn
                    {
                        turn.record(
                            output.prompt_tokens,
                            output.cached_tokens,
                            output.completion_tokens,
                        );
                    }
                    match result {
                        Ok(_) if self.stop_requested => {
                            self.generating = false;
                            self.activity = Some("Stopped".into());
                            start_queued =
                                !self.queued_inputs.is_empty() || self.manual_compaction_queued;
                        }
                        Ok(_) => {
                            let model = self.config.model;
                            let protocol_recovery = if self
                                .conversation
                                .last()
                                .is_some_and(|message| has_misplaced_tool_call(model, message))
                            {
                                Some((
                                    TOOL_PROTOCOL_CORRECTION,
                                    "Correcting tool call…",
                                    "The model emitted a complete tool call inside private reasoning twice; no tool was executed.",
                                ))
                            } else if self.conversation.last().is_some_and(is_empty_response) {
                                Some((
                                    EMPTY_RESPONSE_CORRECTION,
                                    "Retrying empty response…",
                                    "The model returned an empty response twice; generation stopped.",
                                ))
                            } else {
                                None
                            };
                            if let Some((correction, activity, repeated_error)) = protocol_recovery
                            {
                                self.generating = false;
                                self.tool_cards.clear();
                                self.active_generation = None;
                                if correction_already_sent(&self.conversation, correction) {
                                    self.activity = Some("Failed".into());
                                    self.error = Some(repeated_error.into());
                                } else {
                                    self.activity = Some(activity.into());
                                    continuation_feedback = Some(correction.to_owned());
                                }
                                break;
                            }
                            let (validation_errors, replies, open_urls, error_surface_id) = self
                                .conversation
                                .last_mut()
                                .map(|message| {
                                    let surface_id = crate::a2ui::extract_lines(&message.content)
                                        .into_iter()
                                        .rev()
                                        .filter_map(|line| line.value.ok())
                                        .find_map(|value| {
                                            crate::a2ui::message_surface_id(&value)
                                                .map(str::to_owned)
                                        })
                                        .unwrap_or_else(|| "unknown".to_owned());
                                    (
                                        std::mem::take(&mut message.a2ui_errors),
                                        std::mem::take(&mut message.a2ui_replies),
                                        std::mem::take(&mut message.a2ui_open_urls),
                                        surface_id,
                                    )
                                })
                                .unwrap_or_default();
                            for url in open_urls {
                                if let Err(error) =
                                    std::process::Command::new("open").arg(url).spawn()
                                {
                                    self.error =
                                        Some(format!("Could not open the A2UI link: {error}"));
                                }
                            }
                            if !validation_errors.is_empty() {
                                self.generating = false;
                                self.activity = Some("Correcting A2UI…".into());
                                self.tool_cards.clear();
                                let mut feedback = serde_json::json!({
                                    "version": crate::a2ui::VERSION,
                                    "error": {
                                        "code": "VALIDATION_FAILED",
                                        "surfaceId": error_surface_id,
                                        "path": "/",
                                        "message": validation_errors.join("; ")
                                    }
                                })
                                .to_string();
                                if self.a2ui.active_surface().is_none() {
                                    feedback.push_str(
                                        "\nThere is no active A2UI surface. Correct this by emitting createSurface with a new surfaceId and a complete root component tree; do not retry updateComponents for an earlier surface.",
                                    );
                                }
                                continuation_feedback = Some(feedback);
                                self.active_generation = None;
                                break;
                            }
                            if !replies.is_empty() {
                                self.generating = false;
                                self.activity = Some("Continuing A2UI function call…".into());
                                self.tool_cards.clear();
                                continuation_feedback = Some(format!(
                                    "A2UI client response:\n{}",
                                    replies
                                        .iter()
                                        .map(ToString::to_string)
                                        .collect::<Vec<_>>()
                                        .join("\n")
                                ));
                                self.active_generation = None;
                                break;
                            }
                            let model = self.config.model;
                            let content = self
                                .conversation
                                .last()
                                .map(|message| message.content.clone())
                                .unwrap_or_default();
                            match crate::agent::parse_tool_calls(model, &content) {
                                Ok((_, calls)) if !calls.is_empty() => {
                                    if let Err(error) = self.start_agent_tools(calls) {
                                        self.generating = false;
                                        self.error = Some(error);
                                    }
                                }
                                Ok(_) => {
                                    self.generating = false;
                                    self.activity = None;
                                    self.tool_cards.clear();
                                    start_queued = !self.queued_inputs.is_empty()
                                        || self.manual_compaction_queued;
                                }
                                Err(error) => {
                                    self.active_tools = Some(crate::agent::error_async(error));
                                }
                            }
                        }
                        Err(_) if self.stop_requested => {
                            self.generating = false;
                            self.activity = Some("Stopped".into());
                        }
                        Err(error) => {
                            self.generating = false;
                            self.activity = Some("Failed".into());
                            self.error = Some(error);
                        }
                    }
                    self.active_generation = None;
                    break;
                }
                Ok(GenerationEvent::Measured(_)) => {
                    self.generating = false;
                    self.activity = Some("Failed".into());
                    self.error = Some(
                        "The model runtime returned an unexpected context measurement.".into(),
                    );
                }
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => {
                    self.generating = false;
                    self.active_generation = None;
                    self.error = Some("The model runtime stopped unexpectedly.".into());
                    self.activity = Some("Failed".into());
                    break;
                }
            }
        }
        #[cfg(target_os = "macos")]
        if a2ui_changed {
            self.sync_a2ui_renderer_state();
        }
        #[cfg(target_os = "macos")]
        if transcript_changed && let Some(message) = self.conversation.last_mut() {
            message.refresh_markdown();
        }
        #[cfg(target_os = "macos")]
        if transcript_changed
            && let Some(message) = self.conversation.last()
            && let Some(database) = &mut self.database
            && let Err(error) = database.update_message(
                message.id,
                message.reasoning.as_deref(),
                message.reasoning_complete,
                &message.content,
            )
        {
            if let Some(active) = &self.active_generation {
                active.cancel.store(true, Ordering::Relaxed);
            }
            self.error = Some(format!("Could not save generated chat text: {error}"));
        }
        #[cfg(target_os = "macos")]
        if context_changed
            && let Some(session_id) = self.selected_session
            && let Some(database) = &mut self.database
        {
            if let Err(error) = database.update_session_context(
                session_id,
                self.context_used,
                self.context_limit,
                self.tokens_per_second,
            ) {
                self.error = Some(format!("Could not save context usage: {error}"));
            } else if let Some(session) = self
                .projects
                .iter_mut()
                .flat_map(|project| &mut project.sessions)
                .find(|session| session.id == session_id)
            {
                session.context_used = self.context_used as i32;
                session.context_limit = self.context_limit as i32;
                session.last_tokens_per_second = self.tokens_per_second;
            }
        }
        #[cfg(target_os = "macos")]
        if start_queued {
            self.start_next_queued();
        }
        #[cfg(target_os = "macos")]
        if let Some(feedback) = continuation_feedback
            && let Err(error) = self.start_tool_result_check(
                crate::agent::ToolRunResult {
                    content: feedback,
                    touched_paths: Vec::new(),
                },
                ToolCheckStage::ResultInitial,
            )
        {
            self.generating = false;
            self.activity = Some("Failed".into());
            self.error = Some(error);
        }
        #[cfg(target_os = "macos")]
        return transcript_changed;
        #[cfg(not(target_os = "macos"))]
        false
    }

    fn start_next_queued(&mut self) {
        #[cfg(target_os = "macos")]
        if std::mem::take(&mut self.manual_compaction_queued) {
            if let Err(error) =
                self.start_compaction(PendingContinuation::None, "queued manual compaction")
            {
                self.activity = Some("Failed".into());
                self.error = Some(error);
            }
            return;
        }
        if let Some(prompt) = queued_prompt(self.queued_inputs.drain(..)) {
            self.finish_turn_summary();
            self.composer = text_editor::Content::with_text(&prompt);
            self.start_generation();
        }
    }

    #[cfg(target_os = "macos")]
    fn reset_agent_tool_repeats(&mut self) {
        let Some((session_id, tools)) = &self.agent_tools else {
            return;
        };
        if Some(*session_id) != self.selected_session {
            return;
        }
        tools
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .reset_repeated_calls();
    }

    fn finish_turn_summary(&mut self) {
        self.context_notice = None;
        let Some(turn) = self.active_turn.take() else {
            return;
        };
        let Some(message_id) = turn.message_id else {
            return;
        };
        let stats = turn.finish();
        if let Some(message) = self
            .conversation
            .iter_mut()
            .find(|message| message.id == message_id)
        {
            message.generation_stats = Some(stats);
        }
        if let Some(database) = &mut self.database
            && let Err(error) = database.update_message_generation_stats(
                message_id,
                stats.duration_ms,
                stats.input_tokens,
                stats.cached_tokens,
                stats.output_tokens,
            )
        {
            self.error = Some(format!("Could not save turn summary: {error}"));
        }
    }

    #[cfg(target_os = "macos")]
    fn start_agent_tools(&mut self, calls: Vec<crate::agent::ToolCall>) -> Result<(), String> {
        let session_id = self
            .selected_session
            .ok_or_else(|| "The active session is unavailable.".to_owned())?;
        if self.agent_tools.as_ref().map(|(id, _)| *id) != Some(session_id) {
            let project_id = self
                .selected_project
                .ok_or_else(|| "The active project is unavailable.".to_owned())?;
            let root = self
                .projects
                .iter()
                .find(|project| project.project.id == project_id)
                .map(|project| PathBuf::from(&project.project.path))
                .ok_or_else(|| "The active project is unavailable.".to_owned())?;
            let mut tools =
                crate::agent::Tools::new(&root, self.config.active_generation().context_tokens)?;
            if self.config.dev_brain.enabled {
                let projects = self
                    .projects
                    .iter()
                    .map(|project| project.project.clone())
                    .collect::<Vec<_>>();
                tools.enable_dev_brain(&self.config.dev_brain, &projects)?;
            }
            self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
        }
        let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
        let service = self
            .generation_service
            .as_ref()
            .ok_or_else(|| "The model runtime is unavailable.".to_owned())?
            .clone();
        let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
        let mut child_turn = effective.turn.clone();
        child_turn.system_prompt = crate::agent::ralph_system_prompt(
            model,
            &child_turn.system_prompt,
            self.config.dev_brain.enabled,
        );
        for instruction in self
            .model_chat_messages()
            .into_iter()
            .filter(|message| message.instruction_metadata.is_some())
        {
            child_turn.system_prompt.push_str("\n\n");
            child_turn.system_prompt.push_str(&instruction.content);
        }
        if let Some(skills) = self.dev_brain_skills_prompt() {
            child_turn.system_prompt.push_str("\n\n");
            child_turn.system_prompt.push_str(&skills);
        }
        if let Some(skills) = crate::agent::agent_skills_prompt() {
            child_turn.system_prompt.push_str("\n\n");
            child_turn.system_prompt.push_str(&skills);
        }
        tools
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .enable_ralph(
                service.clone(),
                model,
                effective.engine.clone(),
                child_turn,
                idle_timeout,
            );
        let approval_mode = match self.permission_mode {
            PermissionMode::Heuristic => crate::agent::ShellApprovalMode::Heuristic,
            PermissionMode::Ai => {
                crate::agent::ShellApprovalMode::Ai(Box::new(crate::agent::AiRiskClassifier::new(
                    service,
                    effective.engine,
                    effective.turn,
                    transient_cache_path(),
                    idle_timeout,
                )))
            }
        };
        self.tool_cards = calls
            .iter()
            .cloned()
            .map(crate::agent::ToolCard::parsing)
            .collect();
        self.active_tools = Some(crate::agent::execute_async(tools, calls, approval_mode));
        self.activity = Some("Parsing tool calls…".into());
        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn continue_after_tool_result(&mut self) -> Result<(), String> {
        let session_id = self
            .selected_session
            .ok_or_else(|| "The active session is unavailable.".to_owned())?;
        self.skip_compaction_once = false;
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let mut effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
        effective.turn.system_prompt =
            self.chat_system_prompt(model, &effective.turn.system_prompt);
        effective.turn.system_prompt = crate::compaction::summary_system_prompt(
            &effective.turn.system_prompt,
            self.compaction_summary(),
        );
        let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
        let queued = queued_prompt(self.queued_inputs.drain(..));
        if queued.is_some() {
            self.reset_agent_tool_repeats();
            self.a2ui_auto_switch_pending = true;
        }
        let reminders = if self.system_prompt_reminder_due() {
            self.system_prompt_reminders(model)
        } else {
            Vec::new()
        };
        let system_messages = reminders
            .iter()
            .cloned()
            .map(SystemMessage::plain)
            .collect::<Vec<_>>();
        let mut saved = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?
            .continue_durable_tool_turn(
                session_id,
                queued.as_deref(),
                &system_messages,
                assistant_reasoning,
            )
            .map_err(|error| format!("Could not save the tool turn: {error}"))?;
        if !reminders.is_empty() {
            self.system_prompt_seen_at = self.context_used;
        }
        let mut assistant = ChatMessage::from(saved.pop().unwrap());
        for message in saved {
            self.conversation.push(ChatMessage::from(message));
        }
        let messages = self
            .model_chat_messages()
            .into_iter()
            .map(chat_turn)
            .collect();
        assistant.reasoning_open = assistant_reasoning;
        self.conversation.push(assistant);
        self.tool_cards.clear();
        let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
        self.active_generation = Some(
            self.generation_service
                .as_ref()
                .ok_or_else(|| "The model runtime is unavailable.".to_owned())?
                .generate(
                    effective.engine,
                    effective.turn,
                    messages,
                    CheckpointTarget::Local {
                        checkpoint: session_checkpoint_path(session_id),
                        bootstrap: None,
                    },
                    WorkSource::LocalChat,
                    idle_timeout,
                )?,
        );
        self.tokens_per_second = None;
        self.activity = Some("Continuing after tools…".into());
        self.stop_requested = false;
        self.generating = true;
        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn persist_tool_result(&mut self, result: &str) -> Result<(), String> {
        let session_id = self
            .selected_session
            .ok_or_else(|| "The active session is unavailable.".to_owned())?;
        let stored = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?
            .record_tool_result(session_id, result)
            .map_err(|error| format!("Could not save the tool result: {error}"))?;
        self.conversation.push(ChatMessage::from(stored));
        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn persist_workspace_instructions(&mut self, touched_paths: &[PathBuf]) -> Result<(), String> {
        let session_id = self
            .selected_session
            .ok_or_else(|| "The active session is unavailable.".to_owned())?;
        let messages = self.workspace_instruction_messages(touched_paths, false);
        if messages.is_empty() {
            return Ok(());
        }
        let stored = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?
            .record_system_messages(session_id, &messages)
            .map_err(|error| format!("Could not save workspace instructions: {error}"))?;
        self.conversation
            .extend(stored.into_iter().map(ChatMessage::from));
        Ok(())
    }

    #[cfg(target_os = "macos")]
    pub(super) fn stop_agent_jobs(&mut self) {
        let Some((_, tools)) = &self.agent_tools else {
            return;
        };
        let Ok(mut tools) = tools.try_lock() else {
            return;
        };
        let failures = tools.stop_all_jobs();
        if !failures.is_empty() {
            self.error = Some(failures.join("\n"));
        }
    }

    #[cfg(target_os = "macos")]
    fn start_tool_result_check(
        &mut self,
        result: crate::agent::ToolRunResult,
        stage: ToolCheckStage,
    ) -> Result<(), String> {
        if matches!(stage, ToolCheckStage::ResultInitial)
            && crate::compaction::should_compact(self.context_used, self.context_limit)
        {
            return self.start_compaction(
                PendingContinuation::Tool(result),
                "soft limit before tool continuation",
            );
        }
        if matches!(stage, ToolCheckStage::Instructions)
            && crate::compaction::should_compact(self.context_used, self.context_limit)
        {
            return self.start_compaction(
                PendingContinuation::DurableTool(result.touched_paths),
                "soft limit before instruction continuation",
            );
        }
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let mut effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
        effective.turn.system_prompt =
            self.chat_system_prompt(model, &effective.turn.system_prompt);
        effective.turn.system_prompt = crate::compaction::summary_system_prompt(
            &effective.turn.system_prompt,
            self.compaction_summary(),
        );
        let instruction_stage = matches!(
            stage,
            ToolCheckStage::Instructions | ToolCheckStage::InstructionsAfterCompaction
        );
        let mut messages = self
            .model_chat_messages()
            .into_iter()
            .map(chat_turn)
            .collect::<Vec<_>>();
        if !instruction_stage {
            messages.push(ChatTurn {
                user: false,
                tool: true,
                system: false,
                skip_previous_eos: false,
                reasoning: None,
                reasoning_complete: true,
                content: result.content.clone(),
            });
        }
        let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
        let active = self
            .generation_service
            .as_ref()
            .ok_or_else(|| "The model runtime is unavailable.".to_owned())?
            .measure_context(effective.engine, effective.turn, messages, idle_timeout)?;
        self.active_tool_check = Some(ToolResultCheck {
            active,
            result,
            stage,
        });
        self.generating = true;
        self.activity = Some("Checking tool result context…".into());
        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn poll_tool_result_check(&mut self) -> bool {
        let event = self
            .active_tool_check
            .as_mut()
            .unwrap()
            .active
            .events
            .try_recv();
        match event {
            Ok(GenerationEvent::Loading) => {
                self.activity = Some("Loading model…".into());
                false
            }
            Ok(GenerationEvent::Measured(Ok(projected))) => {
                let check = self.active_tool_check.take().unwrap();
                if self.manual_compaction_queued
                    && matches!(check.stage, ToolCheckStage::ResultInitial)
                {
                    self.manual_compaction_queued = false;
                    if let Err(error) = self.start_compaction(
                        PendingContinuation::Tool(check.result),
                        "queued manual compaction",
                    ) {
                        self.generating = false;
                        self.activity = Some("Failed".into());
                        self.error = Some(error);
                    }
                    return true;
                }
                if self.manual_compaction_queued
                    && matches!(check.stage, ToolCheckStage::Instructions)
                {
                    self.manual_compaction_queued = false;
                    if let Err(error) = self.start_compaction(
                        PendingContinuation::DurableTool(check.result.touched_paths),
                        "queued manual compaction",
                    ) {
                        self.generating = false;
                        self.activity = Some("Failed".into());
                        self.error = Some(error);
                    }
                    return true;
                }
                let reserve = if matches!(check.stage, ToolCheckStage::BoundedResult) {
                    16
                } else {
                    crate::compaction::tool_result_reserve(self.context_limit)
                };
                if crate::compaction::tool_result_fits(projected, self.context_limit, reserve) {
                    let continuation = if matches!(
                        check.stage,
                        ToolCheckStage::ResultInitial
                            | ToolCheckStage::ResultAfterCompaction
                            | ToolCheckStage::BoundedResult
                    ) {
                        self.persist_tool_result(&check.result.content)
                            .and_then(|()| {
                                self.persist_workspace_instructions(&check.result.touched_paths)
                            })
                            .and_then(|()| {
                                self.start_tool_result_check(
                                    crate::agent::ToolRunResult {
                                        content: String::new(),
                                        touched_paths: check.result.touched_paths,
                                    },
                                    ToolCheckStage::Instructions,
                                )
                            })
                    } else {
                        self.continue_after_tool_result()
                    };
                    if let Err(error) = continuation {
                        self.generating = false;
                        self.activity = Some("Failed".into());
                        self.error = Some(error);
                    }
                } else {
                    match check.stage {
                        ToolCheckStage::ResultInitial => {
                            if let Err(error) = self.start_compaction(
                                PendingContinuation::Tool(check.result),
                                "tool result would exceed context",
                            ) {
                                self.generating = false;
                                self.activity = Some("Failed".into());
                                self.error = Some(error);
                            }
                        }
                        ToolCheckStage::ResultAfterCompaction => {
                            let content = crate::compaction::bounded_tool_error(
                                projected,
                                self.context_limit,
                                reserve,
                            );
                            let result = crate::agent::ToolRunResult {
                                content,
                                touched_paths: check.result.touched_paths,
                            };
                            if let Err(error) =
                                self.start_tool_result_check(result, ToolCheckStage::BoundedResult)
                            {
                                self.generating = false;
                                self.activity = Some("Failed".into());
                                self.error = Some(error);
                            }
                        }
                        ToolCheckStage::BoundedResult
                        | ToolCheckStage::InstructionsAfterCompaction => {
                            self.generating = false;
                            self.activity = Some("Failed".into());
                            self.error = Some("context full after compaction".into());
                        }
                        ToolCheckStage::Instructions => {
                            if let Err(error) = self.start_compaction(
                                PendingContinuation::DurableTool(check.result.touched_paths),
                                "workspace instructions would exceed context",
                            ) {
                                self.generating = false;
                                self.activity = Some("Failed".into());
                                self.error = Some(error);
                            }
                        }
                    }
                }
                true
            }
            Ok(GenerationEvent::Measured(Err(error))) => {
                self.active_tool_check = None;
                self.generating = false;
                if self.stop_requested {
                    self.activity = Some("Stopped".into());
                } else {
                    self.activity = Some("Failed".into());
                    self.error = Some(error);
                }
                false
            }
            Ok(GenerationEvent::Activity(activity)) => {
                self.activity = Some(activity.into());
                false
            }
            Ok(_) => false,
            Err(TryRecvError::Empty) => false,
            Err(TryRecvError::Disconnected) => {
                self.active_tool_check = None;
                self.generating = false;
                self.activity = Some("Failed".into());
                self.error = Some("The model runtime stopped unexpectedly.".into());
                false
            }
        }
    }

    #[cfg(target_os = "macos")]
    pub(super) fn start_compaction(
        &mut self,
        pending: PendingContinuation,
        reason: &str,
    ) -> Result<(), String> {
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let mut effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
        effective.turn.system_prompt =
            self.chat_system_prompt(model, &effective.turn.system_prompt);
        let rebuild_system_prompt = effective.turn.system_prompt.clone();
        effective.turn.system_prompt = crate::compaction::summary_system_prompt(
            &effective.turn.system_prompt,
            self.compaction_summary(),
        );
        let model_messages = self.model_chat_messages();
        let message_ids = model_messages.iter().map(|message| message.id).collect();
        let messages = model_messages.into_iter().map(chat_turn).collect();
        let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
        let active = self
            .generation_service
            .as_ref()
            .ok_or_else(|| "The model runtime is unavailable.".to_owned())?
            .compact(CompactionInput {
                engine: effective.engine,
                turn: effective.turn,
                messages,
                reason: reason.to_owned(),
                rebuild_system_prompt,
                checkpoint: session_compaction_checkpoint_path(
                    self.selected_session
                        .ok_or_else(|| "The active session is unavailable.".to_owned())?,
                ),
                idle_timeout,
            })?;
        self.active_compaction = Some(CompactionRequest {
            active,
            pending,
            message_ids,
        });
        self.manual_compaction_queued = false;
        self.generating = true;
        self.activity = Some("Compacting durable task state…".into());
        self.tokens_per_second = None;
        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn poll_compaction(&mut self) -> bool {
        let Some(request) = &mut self.active_compaction else {
            return false;
        };
        loop {
            match request.active.events.try_recv() {
                Ok(GenerationEvent::Loading) => self.activity = Some("Loading model…".into()),
                Ok(GenerationEvent::Activity(activity)) => self.activity = Some(activity.into()),
                Ok(GenerationEvent::Context {
                    used,
                    limit,
                    tokens_per_second,
                }) => {
                    self.context_used = used;
                    self.context_limit = limit;
                    self.tokens_per_second = tokens_per_second;
                }
                Ok(GenerationEvent::Compacted(result)) => {
                    let request = self.active_compaction.take().unwrap();
                    match result {
                        Ok(compacted) => {
                            if let Err(error) =
                                self.apply_compaction(&compacted, &request.message_ids)
                            {
                                let _ = fs::remove_file(&compacted.checkpoint);
                                self.generating = false;
                                self.activity = Some("Failed".into());
                                self.error = Some(error);
                                return false;
                            }
                            self.context_used = compacted.context_tokens;
                            self.system_prompt_seen_at = compacted.context_tokens;
                            self.generating = false;
                            self.activity = None;
                            match request.pending {
                                PendingContinuation::None => self.start_next_queued(),
                                PendingContinuation::User(prompt) => {
                                    self.composer = text_editor::Content::with_text(&prompt);
                                    self.skip_compaction_once = true;
                                    self.start_generation();
                                }
                                PendingContinuation::Tool(result) => {
                                    if let Err(error) = self.start_tool_result_check(
                                        result,
                                        ToolCheckStage::ResultAfterCompaction,
                                    ) {
                                        self.generating = false;
                                        self.activity = Some("Failed".into());
                                        self.error = Some(error);
                                    }
                                }
                                PendingContinuation::DurableTool(touched_paths) => {
                                    let continuation = self
                                        .persist_workspace_instructions(&touched_paths)
                                        .and_then(|()| {
                                            self.start_tool_result_check(
                                                crate::agent::ToolRunResult {
                                                    content: String::new(),
                                                    touched_paths,
                                                },
                                                ToolCheckStage::InstructionsAfterCompaction,
                                            )
                                        });
                                    if let Err(error) = continuation {
                                        self.generating = false;
                                        self.activity = Some("Failed".into());
                                        self.error = Some(error);
                                    }
                                }
                            }
                            return true;
                        }
                        Err(error) => {
                            self.generating = false;
                            if let PendingContinuation::User(prompt) = request.pending {
                                if self.composer.text().trim().is_empty() {
                                    self.composer = text_editor::Content::with_text(&prompt);
                                } else {
                                    self.queued_inputs.push_front(prompt);
                                }
                            }
                            if self.stop_requested {
                                self.activity = Some("Stopped".into());
                            } else {
                                self.activity = Some("Failed".into());
                                self.error = Some(error);
                            }
                            return false;
                        }
                    }
                }
                Ok(GenerationEvent::Finished(_))
                | Ok(GenerationEvent::Chunk { .. })
                | Ok(GenerationEvent::Measured(_)) => {}
                Err(TryRecvError::Empty) => return false,
                Err(TryRecvError::Disconnected) => {
                    self.active_compaction = None;
                    self.generating = false;
                    self.activity = Some("Failed".into());
                    self.error = Some("The model runtime stopped unexpectedly.".into());
                    return false;
                }
            }
        }
    }

    #[cfg(target_os = "macos")]
    fn apply_compaction(
        &mut self,
        compacted: &crate::engine::CompactionOutput,
        message_ids: &[i32],
    ) -> Result<(), String> {
        let session_id = self
            .selected_session
            .ok_or_else(|| "The active session is unavailable.".to_owned())?;
        let running_jobs = if let Some((tools_session, tools)) = &self.agent_tools
            && *tools_session == session_id
        {
            tools
                .lock()
                .unwrap_or_else(|poisoned| poisoned.into_inner())
                .compaction_observation()
        } else {
            None
        };
        let archived_session = self
            .projects
            .iter()
            .flat_map(|project| &project.sessions)
            .find(|session| session.id == session_id)
            .is_some_and(|session| session.state() == SessionState::Archived);
        let tail_start = message_ids.get(compacted.tail_start).copied();
        let messages = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?
            .record_compaction(
                session_id,
                &compacted.summary,
                tail_start,
                running_jobs.as_deref(),
                compacted.context_tokens,
                self.context_limit,
            )
            .map_err(|error| format!("Could not save compacted conversation: {error}"))?;
        self.conversation
            .extend(messages.into_iter().map(ChatMessage::from));
        if archived_session {
            self.reload_projects();
        } else if let Some(session) = self
            .projects
            .iter_mut()
            .flat_map(|project| &mut project.sessions)
            .find(|session| session.id == session_id)
        {
            session.compacted_summary = Some(compacted.summary.clone());
            session.context_used = compacted.context_tokens as i32;
            session.context_limit = self.context_limit as i32;
            session.last_tokens_per_second = None;
        }
        let final_checkpoint = session_checkpoint_path(session_id);
        if let Err(error) = fs::rename(&compacted.checkpoint, &final_checkpoint) {
            let _ = fs::remove_file(&compacted.checkpoint);
            self.error = Some(format!(
                "The compacted conversation was saved, but its checkpoint could not be promoted: {error}. It will rebuild on next use."
            ));
        }
        Ok(())
    }

    pub(super) fn compaction_summary(&self) -> Option<&str> {
        if let Some(summary) = self
            .conversation
            .iter()
            .rfind(|message| message.compaction)
            .map(|message| message.content.as_str())
        {
            return Some(summary);
        }
        let session_id = self.selected_session?;
        self.projects
            .iter()
            .flat_map(|project| &project.sessions)
            .find(|session| session.id == session_id)
            .and_then(|session| session.compacted_summary.as_deref())
    }

    fn model_chat_messages(&self) -> Vec<&ChatMessage> {
        let start = compacted_context_start(&self.conversation);
        self.conversation[start..]
            .iter()
            .filter(|message| {
                !message.compaction
                    && !(message.system && message.content.starts_with(LEGACY_AGENTS_PREFIX))
                    && !(message.instruction_metadata.is_some() && message.content.is_empty())
            })
            .collect()
    }

    /// Asks the model for a session title without opening a session for it: the
    /// turn runs against the shared transient KV cache and only its text is kept.
    ///
    /// Reports failures to the caller instead of the error banner: the automatic
    /// first-chat titling must stay silent, while the menu action shows them.
    #[cfg(target_os = "macos")]
    fn request_title(&mut self, session_id: i32) -> Result<(), String> {
        if self.active_titling.is_some() {
            return Err("A title is already being generated.".into());
        }
        let model = self.config.model;
        let generation = self.config.active_generation();
        let runtime = self.config.runtime_for(model);
        let mut effective =
            crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
        let database = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?;
        let stored = database
            .load_messages(session_id)
            .map_err(|error| format!("Could not read the session: {error}"))?;
        // The assistant row is inserted empty when a turn starts; an empty turn
        // is never useful context, and skipping it is what lets the automatic
        // pass summarize the user's opening message on its own.
        let messages = stored
            .into_iter()
            .filter(|message| !message.compaction)
            .map(|message| ChatTurn {
                user: message.user,
                tool: message.tool,
                system: message.system,
                skip_previous_eos: false,
                reasoning: None,
                reasoning_complete: true,
                content: message.content,
            });
        let mut messages = title_context(messages);
        if messages.is_empty() {
            return Err("This session has no messages to summarize yet.".into());
        }
        messages.push(ChatTurn {
            user: true,
            tool: false,
            system: false,
            skip_previous_eos: false,
            reasoning: None,
            reasoning_complete: true,
            content: TITLE_INSTRUCTION.to_owned(),
        });
        effective.turn.max_generated_tokens = TITLE_MAX_TOKENS;
        effective.turn.reasoning_mode = ReasoningMode::Direct;

        let service = self
            .generation_service
            .as_ref()
            .ok_or_else(|| "The model runtime is unavailable.".to_owned())?;
        let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
        match service.generate(
            effective.engine,
            effective.turn,
            messages,
            CheckpointTarget::Transient(transient_cache_path()),
            WorkSource::LocalChat,
            idle_timeout,
        ) {
            Ok(active) => {
                self.active_titling = Some(TitleRequest {
                    session_id,
                    expected: self.session_title(session_id).unwrap_or_default(),
                    active,
                    content: String::new(),
                });
                Ok(())
            }
            Err(error) => {
                self.generation_service = None;
                Err(error)
            }
        }
    }

    /// The "Retitle with AI" menu action, which reports why it could not run.
    #[cfg(target_os = "macos")]
    pub(super) fn retitle_session(&mut self, session_id: i32) {
        match self.request_title(session_id) {
            Ok(()) => self.error = None,
            Err(error) => self.error = Some(error),
        }
    }

    /// Titles a session from its opening message. Queued behind the reply that
    /// is already running, and skipped without complaint if it cannot start —
    /// the user did not ask for this, so it must never interrupt them.
    #[cfg(target_os = "macos")]
    pub(super) fn request_first_title(&mut self, session_id: i32) {
        let _ = self.request_title(session_id);
    }

    /// Drains the one-shot title generation. Returns true while it is running so
    /// the caller keeps ticking.
    #[cfg(target_os = "macos")]
    pub(super) fn poll_titling(&mut self) -> bool {
        let Some(mut request) = self.active_titling.take() else {
            return false;
        };
        let failure = loop {
            match request.active.events.try_recv() {
                Ok(GenerationEvent::Chunk {
                    reasoning: false,
                    content,
                }) => request.content.push_str(&content),
                Ok(GenerationEvent::Finished(Err(error))) => break Some(error),
                Ok(GenerationEvent::Finished(Ok(_))) => break None,
                Ok(_) => {}
                Err(TryRecvError::Empty) => {
                    self.active_titling = Some(request);
                    return true;
                }
                Err(TryRecvError::Disconnected) => {
                    break Some("The model runtime stopped unexpectedly.".to_owned());
                }
            }
        };
        if let Some(error) = failure {
            self.error = Some(format!("Could not generate a title: {error}"));
        } else if let Some(title) = session_title(&request.content) {
            self.apply_title(&request, &title);
        } else {
            self.error = Some("The model did not return a usable title.".into());
        }
        false
    }

    pub(super) fn session_title(&self, session_id: i32) -> Option<String> {
        self.projects
            .iter()
            .flat_map(|project| &project.sessions)
            .find(|session| session.id == session_id)
            .map(|session| session.title.clone())
    }

    #[cfg(target_os = "macos")]
    fn apply_title(&mut self, request: &TitleRequest, title: &str) {
        let Some(current) = self.session_title(request.session_id) else {
            return;
        };
        if current != request.expected {
            return;
        }
        let Some(database) = &mut self.database else {
            return;
        };
        match database.rename_session(request.session_id, title) {
            Ok(()) => {
                self.error = None;
                self.reload_projects();
            }
            Err(error) => self.error = Some(format!("Could not save the session title: {error}")),
        }
    }
}

/// Reduces a model reply to a single sidebar-sized line, or `None` if nothing
/// usable came back.
pub(super) fn session_title(reply: &str) -> Option<String> {
    let title = reply
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty())?
        .trim_matches(|character: char| {
            character.is_whitespace() || matches!(character, '"' | '\'' | '`' | '*' | '#' | '.')
        })
        .trim();
    if title.is_empty() {
        return None;
    }
    Some(match title.char_indices().nth(TITLE_MAX_CHARS) {
        Some((index, _)) => format!("{}…", title[..index].trim_end()),
        None => title.to_owned(),
    })
}

#[cfg(test)]
mod tests {
    use super::{
        ChatMessage, TOOL_PROTOCOL_CORRECTION, TurnSummary, chat_turn, compacted_context_start,
        correction_already_sent, has_chat_after_last_compaction, has_misplaced_tool_call,
        is_empty_response, promote_legacy_turn_summaries, queued_prompt, sync_a2ui_message,
        title_context,
    };
    use crate::engine::ChatTurn;
    use crate::model::ModelChoice;

    fn assistant(reasoning: Option<&str>, content: &str) -> ChatMessage {
        ChatMessage {
            id: 1,
            user: false,
            tool: false,
            system: false,
            compaction: false,
            compaction_tail_start: None,
            generation_stats: None,
            reasoning: reasoning.map(str::to_owned),
            reasoning_complete: false,
            reasoning_open: false,
            content: content.to_owned(),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: iced::widget::markdown::Content::new(),
            transcript: iced::widget::text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        }
    }

    fn turn(user: bool, system: bool, content: &str) -> ChatTurn {
        ChatTurn {
            user,
            tool: false,
            system,
            skip_previous_eos: false,
            reasoning: None,
            reasoning_complete: true,
            content: content.to_owned(),
        }
    }

    #[test]
    fn turn_summary_accumulates_model_continuations() {
        let mut summary = TurnSummary::new();
        summary.record(100, 80, 20);
        summary.record(140, 120, 30);

        let stats = summary.finish();
        assert_eq!(stats.input_tokens, 240);
        assert_eq!(stats.cached_tokens, 200);
        assert_eq!(stats.output_tokens, 50);
    }

    #[test]
    fn model_history_uses_persisted_content_without_changing_the_transcript() {
        let mut user = assistant(None, "visible question");
        user.user = true;
        user.model_content = Some("visible question\n\nhidden metadata".into());

        assert_eq!(
            chat_turn(&user).content,
            "visible question\n\nhidden metadata"
        );
        assert_eq!(user.content, "visible question");
    }

    #[test]
    fn legacy_generation_rows_collapse_into_their_user_turn() {
        let mut user = assistant(None, "question");
        user.user = true;
        let mut first = assistant(None, "tool call");
        first.generation_stats = Some(super::GenerationStats {
            duration_ms: 1_000,
            input_tokens: 100,
            cached_tokens: 80,
            output_tokens: 20,
        });
        let mut second = assistant(None, "answer");
        second.generation_stats = Some(super::GenerationStats {
            duration_ms: 2_000,
            input_tokens: 140,
            cached_tokens: 120,
            output_tokens: 30,
        });
        let mut messages = [user, first, second];

        promote_legacy_turn_summaries(&mut messages);

        assert_eq!(
            messages[0].generation_stats,
            Some(super::GenerationStats {
                duration_ms: 3_000,
                input_tokens: 240,
                cached_tokens: 200,
                output_tokens: 50,
            })
        );
        assert!(messages[1].generation_stats.is_none());
        assert!(messages[2].generation_stats.is_none());
    }

    #[test]
    fn title_context_starts_at_the_first_user_and_ignores_system_rows() {
        let messages = title_context([
            turn(false, true, "project AGENTS.md"),
            turn(false, true, "date and time"),
            turn(true, false, "actual request"),
            turn(false, true, "system reminder"),
            turn(false, false, "answer"),
        ]);

        assert_eq!(messages.len(), 2);
        assert_eq!(messages[0].content, "actual request");
        assert_eq!(messages[1].content, "answer");
    }

    #[test]
    fn complete_tool_call_in_reasoning_gets_one_correction() {
        let call = r#"<｜DSML｜tool_calls>
<｜DSML｜invoke name="bash">
<｜DSML｜parameter name="command" string="true">pwd</｜DSML｜parameter>
</｜DSML｜invoke>
</｜DSML｜tool_calls>"#;
        let malformed = assistant(Some(call), "");
        assert!(has_misplaced_tool_call(
            ModelChoice::DeepSeekV4Flash,
            &malformed
        ));
        assert!(!has_misplaced_tool_call(
            ModelChoice::DeepSeekV4Flash,
            &assistant(Some(call), "I was only discussing this call.")
        ));
        assert!(is_empty_response(&assistant(Some(""), "")));
        assert!(!is_empty_response(&assistant(Some("still thinking"), "")));

        let mut correction = assistant(None, TOOL_PROTOCOL_CORRECTION);
        correction.tool = true;
        let conversation = vec![assistant(Some(call), ""), correction, malformed];
        assert!(correction_already_sent(
            &conversation,
            TOOL_PROTOCOL_CORRECTION
        ));

        let mut result = assistant(None, "Tool result 1 (bash):\nok\n");
        result.tool = true;
        let mut prior_correction = assistant(None, TOOL_PROTOCOL_CORRECTION);
        prior_correction.tool = true;
        let conversation = vec![
            assistant(Some(call), ""),
            prior_correction,
            assistant(None, call),
            result,
            assistant(Some(call), ""),
        ];
        assert!(!correction_already_sent(
            &conversation,
            TOOL_PROTOCOL_CORRECTION
        ));
    }

    #[test]
    fn queued_guidance_is_one_reference_style_user_turn() {
        assert_eq!(queued_prompt(Vec::new()), None);
        assert_eq!(queued_prompt(["one".to_owned()]), Some("one".to_owned()));
        assert_eq!(
            queued_prompt(["one".to_owned(), "two".to_owned()]),
            Some("Queued user message 1:\none\n\nQueued user message 2:\ntwo".to_owned())
        );
    }

    #[test]
    fn assistant_markdown_keeps_llm_tables() {
        let mut message = ChatMessage {
            id: 1,
            user: false,
            tool: false,
            system: false,
            compaction: false,
            compaction_tail_start: None,
            generation_stats: None,
            reasoning: None,
            reasoning_complete: true,
            reasoning_open: false,
            content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
                .to_owned(),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: iced::widget::markdown::Content::new(),
            transcript: iced::widget::text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        };

        message.refresh_markdown();

        let table = message.markdown.items().iter().find_map(|item| {
            if let iced::widget::markdown::Item::Table { columns, rows } = item {
                Some((columns, rows))
            } else {
                None
            }
        });
        let (columns, rows) = table.expect("the completed Markdown table must remain renderable");
        assert_eq!(columns.len(), 2);
        assert_eq!(rows.len(), 2);
    }

    #[test]
    fn a2ui_auto_switch_waits_for_a_renderable_surface() {
        let mut store = crate::a2ui::Store::default();
        let mut database = None;
        let mut message = assistant(
            None,
            r#"```a2ui
{"version":"v1.0","createSurface":{"surfaceId":"answer","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}
```"#,
        );
        assert!(!sync_a2ui_message(
            &mut store,
            &mut database,
            None,
            &mut message
        ));

        message.content = r#"```a2ui
{"version":"v1.0","createSurface":{"surfaceId":"answer","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}
{"version":"v1.0","updateComponents":{"surfaceId":"answer","components":[{"id":"root","component":"Text","text":"Ready"}]}}
```"#
            .to_owned();
        assert!(sync_a2ui_message(
            &mut store,
            &mut database,
            None,
            &mut message
        ));
    }

    #[test]
    fn last_compaction_selects_its_tail_without_hiding_history() {
        let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
            id,
            user: !compaction,
            tool: false,
            system: compaction,
            compaction,
            compaction_tail_start: tail,
            generation_stats: None,
            reasoning: None,
            reasoning_complete: true,
            reasoning_open: false,
            content: format!("message {id}"),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: iced::widget::markdown::Content::new(),
            transcript: iced::widget::text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        };
        let history = vec![
            message(1, false, None),
            message(2, false, None),
            message(3, true, Some(2)),
            message(4, false, None),
            message(5, true, Some(4)),
            message(6, false, None),
            message(7, true, Some(6)),
            message(8, false, None),
        ];

        assert_eq!(history.len(), 8);
        assert_eq!(compacted_context_start(&history), 5);
        assert_eq!(history[compacted_context_start(&history)].id, 6);
    }

    #[test]
    fn forced_compaction_requires_new_visible_chat() {
        let message =
            |id: i32, user: bool, tool: bool, system: bool, compaction: bool| ChatMessage {
                id,
                user,
                tool,
                system,
                compaction,
                compaction_tail_start: None,
                generation_stats: None,
                reasoning: None,
                reasoning_complete: true,
                reasoning_open: false,
                content: format!("message {id}"),
                model_content: None,
                tool_approval_reasons: Vec::new(),
                instruction_metadata: None,
                markdown: iced::widget::markdown::Content::new(),
                transcript: iced::widget::text_editor::Content::new(),
                a2ui_lines_processed: 0,
                a2ui_errors: Vec::new(),
                a2ui_replies: Vec::new(),
                a2ui_open_urls: Vec::new(),
            };
        let mut history = vec![
            message(1, true, false, false, false),
            message(2, false, false, true, true),
            ChatMessage {
                content: format!(
                    "{} running job",
                    crate::agent::COMPACTION_OBSERVATION_PREFIX
                ),
                ..message(3, false, true, false, false)
            },
            message(4, false, false, true, false),
        ];

        assert!(!has_chat_after_last_compaction(&history));
        history.push(message(5, false, true, false, false));
        assert!(has_chat_after_last_compaction(&history));
        history.push(message(6, false, false, true, true));
        assert!(!has_chat_after_last_compaction(&history));
        history.push(message(7, true, false, false, false));
        assert!(has_chat_after_last_compaction(&history));
    }
}
