use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    process::{Command, Stdio},
    sync::{
        Arc,
        atomic::AtomicBool,
        mpsc::{Receiver, Sender},
    },
    thread::JoinHandle,
    time::{Duration, Instant},
};

use cms_v2::{cms_api::CommitResult, prompt_cache::CachedPromptEnvelope};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use serde_json::{Value, json};

use crate::{
    attachments::{attachment_for, extract_attachments},
    command_registry::CommandRegistry,
    core::{
        AgentProfileStore, ChatMessage, ConfigStore, HbseServiceRef, HbseServiceRefStore,
        McpConfigStore, McpServerConfig, McpToolConfig, McpTransport, ModelRegistry,
        ProviderConfig, ProviderRegistry, SessionManager, SessionState, SessionStore,
        default_system_prompt, default_tool_definitions, load_skill_definitions,
    },
    environment::get_env,
    guardrails::{
        ApprovalRequest, GuardrailEngine, PermissionPolicy, command_name_from_args,
        default_allowed_commands, normalize_command_name,
    },
    lsl::{LslSkillTrace, append_skill_trace, update_skill_metrics_for_load},
    mcp::{load_mcp_servers, register_mcp_tools},
    memory::{VegvisirCms, VegvisirCmsConfig, default_vegvisir_data_root},
    model_discovery::discover_provider_models,
    observability::EventLogger,
    parallelism::ParallelismConfig,
    persona::{KA_PROMPT_HEADING, get_persona_with_root, render_persona_prompt_section},
    policy::RuntimePolicy,
    profile::{UserProfile, UserProfileStore},
    provider::{
        ConversationRunner, ProviderRouter, ProviderRunEvent, configured_max_tool_rounds_label,
        direct_provider_auth_allowed, set_runtime_max_tool_rounds,
    },
    run_artifacts::{RunArtifactManager, RunFailure, RunManifest, RunStatus},
    speech::{ActiveSpeechRecording, DEFAULT_PTT_KEY, DEFAULT_PTT_SECONDS, PushToTalkKey},
    subagents::{SubAgentStatus, SubAgentTaskRecord},
    tools::{
        DEFAULT_ACTIVE_SUBAGENT_LIMIT, SubagentProviderDefaults, ToolExecutor, ToolRegistry,
        build_builtin_registry_with_cms_mode_subagent_limit_and_provider_defaults,
    },
    types::ToolCall,
    ui::{
        input::{InputState, Suggestion},
        layout::LayoutRenderer,
    },
};

mod commands;
mod input;
mod lsl_runtime;
mod runtime;
mod shell;
mod tui_loop;
mod util;
mod workspace_state;

use lsl_runtime::{compiled_lsl_selected_from_trace, prepare_lsl_augmented_content};
pub use tui_loop::{run_tui, run_tui_with_dangerous_bypass};
pub(crate) use util::is_turn_failure_summary;
pub use util::workspace_project_id;
use util::*;

pub struct TuiApplication {
    pub cwd: PathBuf,
    pub data_root: PathBuf,
    pub session: SessionState,
    pub sessions: SessionManager,
    pub agents: AgentProfileStore,
    pub config: ConfigStore,
    pub commands: CommandRegistry,
    pub provider_registry: ProviderRegistry,
    pub models: ModelRegistry,
    pub cms: VegvisirCms,
    pub tool_registry: ToolRegistry,
    pub tool_executor: ToolExecutor,
    pub profile_store: UserProfileStore,
    pub user_profile: UserProfile,
    pub logger: EventLogger,
    pub parallelism: ParallelismConfig,
    pub input: InputState,
    pub renderer: LayoutRenderer,
    pub chat_scroll_offset: usize,
    pub running: bool,
    pub clear_requested: bool,
    pub redraw_requested: bool,
    pub risky_tools_enabled: bool,
    pub dangerously_bypass_approvals_and_sandbox: bool,
    pub autonomous_mode_enabled: bool,
    pub autonomous_level: usize,
    pub active_subagent_limit: usize,
    pub subagent_provider_defaults: SubagentProviderDefaults,
    pub mcp_servers: Vec<crate::core::McpServerConfig>,
    pub hbse_services: Vec<HbseServiceRef>,
    pub pending_send: Option<JoinHandle<anyhow::Result<SessionState>>>,
    pending_background_jobs: Vec<JoinHandle<anyhow::Result<String>>>,
    pending_speech_jobs: Vec<JoinHandle<anyhow::Result<crate::speech::SpeechTranscriptionResult>>>,
    active_speech_recording: Option<ActiveSpeechRecording>,
    pub speech_ptt_key: Option<PushToTalkKey>,
    pub speech_ptt_seconds: u64,
    pending_stream: Option<Receiver<StreamEvent>>,
    pending_cancel: Option<Arc<AtomicBool>>,
    pending_run_artifact: Option<(RunArtifactManager, RunManifest)>,
    pending_steering: Option<Sender<String>>,
    pending_turn_started_at: Option<Instant>,
    pending_turn_last_activity_at: Option<Instant>,
    pub pending_editor_action: Option<PendingEditorAction>,
    pub command_palette_open: bool,
    pub help_overlay_open: bool,
    pub diff_overlay: Option<DiffOverlay>,
    pub diff_scroll_offset: usize,
    pub info_overlay: Option<InfoOverlay>,
    pub info_scroll_offset: usize,
    pub sessions_overlay: Option<SessionsOverlay>,
    pub profile_overlay: Option<ProfileOverlay>,
    pub approval_selected_index: usize,
    pub search_open: bool,
    pub search_query: String,
    pub search_match_index: usize,
    pub mouse_capture_enabled: bool,
    pub chat_area_x: u16,
    pub chat_area_y: u16,
    pub chat_area_width: u16,
    pub chat_area_height: u16,
    pub chat_render_scroll: usize,
    pub chat_rendered_lines: Vec<String>,
    pub chat_render_cache: ChatTranscriptRenderCache,
    pub drag_anchor: Option<(u16, u16)>,
    pub drag_current: Option<(u16, u16)>,
    pub autonomy: AutonomyState,
    pub observed_subagent_transcript_signatures: BTreeMap<String, String>,
    pub last_subagent_board_poll: Option<std::time::Instant>,
}

#[derive(Clone, Debug)]
pub struct HeadlessObservedRun {
    pub response: String,
    pub events: Vec<ProviderRunEvent>,
    pub prompt_envelope: CachedPromptEnvelope,
    pub memory_write_results: Vec<CommitResult>,
    pub memory_write_error: Option<String>,
}

enum StreamEvent {
    PromptEnvelope(Box<CachedPromptEnvelope>),
    Delta(String),
    Activity(String),
    ToolStart {
        name: String,
        args: String,
    },
    ToolEnd {
        name: String,
        ok: bool,
        summary: String,
        detail: Option<String>,
    },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingEditorAction {
    pub kind: PendingEditorKind,
    pub id: String,
    pub path: PathBuf,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PendingEditorKind {
    KaProfile,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffRenderer {
    Unified,
    Delta,
    Difftastic,
}

impl DiffRenderer {
    pub fn label(self) -> &'static str {
        match self {
            DiffRenderer::Unified => "unified",
            DiffRenderer::Delta => "delta",
            DiffRenderer::Difftastic => "difftastic",
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ChatTranscriptRenderCache {
    pub width: usize,
    pub search_query: String,
    pub message_lines: Vec<Option<ChatMessageRenderCacheEntry>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChatMessageRenderCacheEntry {
    pub key: ChatMessageRenderCacheKey,
    pub lines: Vec<ratatui::text::Line<'static>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChatMessageRenderCacheKey {
    pub role: String,
    pub role_label_hash: u64,
    pub created_at_millis: i64,
    pub content_len: usize,
    pub content_hash: u64,
    pub thinking_trace_visible: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffOverlay {
    pub title: String,
    pub diff: String,
    pub rendered_by: DiffRenderer,
    pub files_changed: usize,
    pub added_lines: usize,
    pub removed_lines: usize,
    pub rendered_lines_cache: Option<DiffOverlayRenderCache>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffOverlayRenderCache {
    pub width: usize,
    pub lines: Vec<ratatui::text::Line<'static>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InfoOverlay {
    pub title: String,
    pub body: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionsOverlay {
    pub workspace: String,
    pub entries: Vec<SessionsOverlayEntry>,
    pub selected: usize,
    pub status: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionsOverlayEntry {
    pub session_id: String,
    pub title: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub message_count: usize,
    pub current: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProfileOverlay {
    pub fields: Vec<ProfileOverlayField>,
    pub selected: usize,
    pub status: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProfileOverlayField {
    pub key: String,
    pub label: String,
    pub value: String,
    pub hint: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AutonomyState {
    pub enabled: bool,
    pub active: bool,
    pub objective: String,
    pub max_steps: usize,
    pub step: usize,
    pub last_status: String,
    pub last_signature: Option<String>,
    pub no_progress_count: usize,
    pub last_turn_had_tools: bool,
    pub plan_path: Option<String>,
    pub cll_path: Option<String>,
    pub pll_path: Option<String>,
    pub manifest_path: Option<String>,
    pub state_path: Option<String>,
    pub journal_path: Option<String>,
    pub current_node_id: Option<String>,
    pub current_node_title: Option<String>,
    pub node_total: usize,
    pub node_completed: usize,
    pub current_evidence_path: Option<String>,
    pub current_evidence_valid: bool,
    pub current_evidence_status: Option<String>,
    pub current_evidence_blocked: bool,
    pub current_evidence_partial: bool,
    pub current_evidence_errors: Vec<String>,
    pub current_node_attempts: usize,
    pub max_node_attempts: usize,
    pub checklist_total: usize,
    pub checklist_completed: usize,
}

impl Default for AutonomyState {
    fn default() -> Self {
        Self {
            enabled: false,
            active: false,
            objective: String::new(),
            max_steps: 12,
            step: 0,
            last_status: "off".to_string(),
            last_signature: None,
            no_progress_count: 0,
            last_turn_had_tools: false,
            plan_path: None,
            cll_path: None,
            pll_path: None,
            manifest_path: None,
            state_path: None,
            journal_path: None,
            current_node_id: None,
            current_node_title: None,
            node_total: 0,
            node_completed: 0,
            current_evidence_path: None,
            current_evidence_valid: false,
            current_evidence_status: None,
            current_evidence_blocked: false,
            current_evidence_partial: false,
            current_evidence_errors: Vec::new(),
            current_node_attempts: 0,
            max_node_attempts: 3,
            checklist_total: 0,
            checklist_completed: 0,
        }
    }
}

impl ProfileOverlay {
    pub fn from_profile(profile: &UserProfile) -> Self {
        let fields = vec![
            ProfileOverlayField::new(
                "name",
                "Display name",
                &profile.identity.display_name,
                "Your name as shown in profile summaries",
            ),
            ProfileOverlayField::new(
                "address_as",
                "Address as",
                &profile.identity.address_as,
                "Name Vegvisir should use when speaking to you",
            ),
            ProfileOverlayField::new(
                "pronouns",
                "Pronouns",
                &profile.identity.pronouns,
                "Optional pronouns, e.g. they/them, she/her, he/him",
            ),
            ProfileOverlayField::new(
                "spoken_language",
                "Spoken language",
                &profile.communication.spoken_language,
                "Default human language, e.g. en, es, fr",
            ),
            ProfileOverlayField::new(
                "response_language",
                "Response language",
                &profile.communication.response_language,
                "Language Vegvisir should answer in; blank uses spoken language",
            ),
            ProfileOverlayField::new(
                "tone",
                "Tone/style",
                &profile.communication.tone,
                "Communication style, e.g. direct, technical, concise",
            ),
            ProfileOverlayField::new(
                "verbosity",
                "Verbosity",
                &profile.communication.verbosity,
                "short, medium, detailed, etc.",
            ),
            ProfileOverlayField::new(
                "coding_languages",
                "Coding languages",
                &profile.coding.coding_languages.join(", "),
                "Comma-separated preferred coding languages",
            ),
            ProfileOverlayField::new(
                "spoken_languages",
                "Other spoken langs",
                &profile.coding.spoken_languages.join(", "),
                "Comma-separated additional spoken languages",
            ),
            ProfileOverlayField::new(
                "comment_language",
                "Code comments",
                &profile.coding.comment_language,
                "Preferred language for code comments",
            ),
            ProfileOverlayField::new(
                "code_style",
                "Code style",
                &profile.coding.code_style,
                "Style preference; blank means match repository style",
            ),
            ProfileOverlayField::new(
                "test_preference",
                "Test preference",
                &profile.coding.test_preference,
                "How Vegvisir should verify changes",
            ),
            ProfileOverlayField::new(
                "use_name_in_chat",
                "Use name in chat",
                bool_text(profile.communication.use_name_in_chat),
                "true/false",
            ),
            ProfileOverlayField::new(
                "show_code_and_diffs_in_chat",
                "Show code/diffs",
                bool_text(profile.workflow.show_code_and_diffs_in_chat),
                "true/false",
            ),
            ProfileOverlayField::new(
                "commit_and_push_when_clean",
                "Commit/push clean",
                bool_text(profile.workflow.commit_and_push_when_clean),
                "true/false",
            ),
            ProfileOverlayField::new(
                "prefer_direct_implementation",
                "Direct implementation",
                bool_text(profile.workflow.prefer_direct_implementation),
                "true/false",
            ),
        ];
        Self {
            fields,
            selected: 0,
            status: "Fill out your local profile. Enter saves, Esc cancels.".to_string(),
        }
    }
}

impl ProfileOverlayField {
    fn new(key: &str, label: &str, value: &str, hint: &str) -> Self {
        Self {
            key: key.to_string(),
            label: label.to_string(),
            value: value.to_string(),
            hint: hint.to_string(),
        }
    }
}

fn bool_text(value: bool) -> &'static str {
    if value { "true" } else { "false" }
}

fn command_matches_palette_query(name: &str, description: &str, raw: &str) -> bool {
    let query = raw.trim().trim_start_matches('/').to_ascii_lowercase();
    if query.is_empty() {
        return true;
    }
    let name = name.to_ascii_lowercase();
    let description = description.to_ascii_lowercase();
    name.trim_start_matches('/').starts_with(&query)
        || name.contains(&query)
        || description.contains(&query)
}

fn should_refresh_suggestions_before_key(key: &KeyEvent) -> bool {
    !matches!(
        key.code,
        KeyCode::Enter | KeyCode::Up | KeyCode::Down | KeyCode::Tab
    )
}

fn diff_overlay_from_patch(title: &str, diff: &str) -> DiffOverlay {
    diff_overlay_from_rendered(title, diff, DiffRenderer::Unified)
}

fn diff_overlay_from_rendered(title: &str, diff: &str, rendered_by: DiffRenderer) -> DiffOverlay {
    let files_changed = diff
        .lines()
        .filter(|line| line.starts_with("diff --git "))
        .count();
    let added_lines = diff
        .lines()
        .filter(|line| line.starts_with('+') && !line.starts_with("+++"))
        .count();
    let removed_lines = diff
        .lines()
        .filter(|line| line.starts_with('-') && !line.starts_with("---"))
        .count();
    DiffOverlay {
        title: title.to_string(),
        diff: diff.to_string(),
        rendered_by,
        files_changed,
        added_lines,
        removed_lines,
        rendered_lines_cache: None,
    }
}

fn apply_scroll_delta(current: usize, delta: isize) -> usize {
    if delta >= 0 {
        current.saturating_add(delta as usize)
    } else {
        current.saturating_sub(delta.unsigned_abs())
    }
}

fn command_response_is_chat_suppressed(command: &str) -> bool {
    matches!(
        command,
        "/load"
            | "/status"
            | "/sessions"
            | "/agent"
            | "/agents"
            | "/model"
            | "/models"
            | "/provider"
            | "/providers"
    )
}

fn should_show_info_overlay(command: &str, response: &str) -> bool {
    if response.trim().is_empty() {
        return false;
    }
    matches!(
        command,
        "/tools"
            | "/skills"
            | "/context"
            | "/agent"
            | "/agents"
            | "/model"
            | "/models"
            | "/effort"
            | "/fast"
            | "/provider"
            | "/providers"
            | "/projects"
            | "/approvals"
            | "/system"
            | "/system-prompt"
            | "/trace"
            | "/tts"
            | "/status"
            | "/config"
            | "/mcp"
            | "/hbse"
    )
}

#[derive(Clone, Debug)]
pub(crate) struct LslRuntimeConfig {
    mode: String,
    token_budget: usize,
    max_primary_subskills: usize,
    max_total_subskills: usize,
    max_dependency_depth: usize,
    allow_extended: bool,
    semantic_router: bool,
}

struct ProjectListEntry {
    cwd: String,
    latest_session_id: String,
    latest_created_at: chrono::DateTime<chrono::Utc>,
    session_count: usize,
    message_count: usize,
}

impl TuiApplication {
    pub fn new(cwd: impl AsRef<Path>) -> anyhow::Result<Self> {
        Self::new_with_dangerous_bypass(cwd, false)
    }

    pub fn new_with_dangerous_bypass(
        cwd: impl AsRef<Path>,
        dangerously_bypass_approvals_and_sandbox: bool,
    ) -> anyhow::Result<Self> {
        let cwd = cwd.as_ref().to_path_buf();
        let data_root = default_vegvisir_data_root();
        Self::with_data_root_and_dangerous_bypass(
            cwd,
            data_root,
            dangerously_bypass_approvals_and_sandbox,
        )
    }

    pub fn with_data_root(
        cwd: impl AsRef<Path>,
        data_root: impl AsRef<Path>,
    ) -> anyhow::Result<Self> {
        Self::with_data_root_and_dangerous_bypass(cwd, data_root, false)
    }

    pub fn with_data_root_and_dangerous_bypass(
        cwd: impl AsRef<Path>,
        data_root: impl AsRef<Path>,
        dangerously_bypass_approvals_and_sandbox: bool,
    ) -> anyhow::Result<Self> {
        let cwd = cwd.as_ref().to_path_buf();
        let data_root = data_root.as_ref().to_path_buf();
        let config = ConfigStore::new(data_root.join("config.json"));
        let defaults = config.load().unwrap_or_default();
        let default_user_id = configured_user_id(&defaults);
        let tools = default_tool_definitions()?;
        let skills = load_skill_definitions(&cwd, &data_root)?;
        let sessions = SessionManager::new(
            SessionStore::new(session_root_for_user(&data_root, &default_user_id))?,
            &cwd,
        );
        let agents = AgentProfileStore::new(data_root.join("agents"))?;
        let _agent_registration_report =
            commands::agents::ensure_registered_agent_identities(&agents, &cwd, &data_root)?;
        let cms_project_id = workspace_project_id(&cwd);
        let cms = VegvisirCms::open(VegvisirCmsConfig {
            db_path: data_root.join("cms-v2.sqlite3"),
            user_id: default_user_id.clone(),
            project_id: Some(cms_project_id.clone()),
            context_mode: cms_v2::ecm::ContextMode::Project,
            commit_writebacks: true,
        })?;
        let cms_config = VegvisirCmsConfig {
            db_path: data_root.join("cms-v2.sqlite3"),
            user_id: default_user_id,
            project_id: Some(cms_project_id),
            context_mode: cms_v2::ecm::ContextMode::Project,
            commit_writebacks: true,
        };
        let models = ModelRegistry::default_catalog()?;
        let subagent_defaults_config = default_subagent_provider_defaults();
        let active_subagent_limit = defaults
            .get("subagent_active_limit")
            .and_then(Value::as_u64)
            .map(|value| value as usize)
            .unwrap_or_else(default_subagent_active_limit)
            .max(1);
        let subagent_provider = defaults
            .get("subagent_provider")
            .and_then(Value::as_str)
            .unwrap_or(&subagent_defaults_config.provider);
        let configured_subagent_model = defaults
            .get("subagent_model")
            .and_then(Value::as_str)
            .unwrap_or(&subagent_defaults_config.model);
        let subagent_model = if model_known_invalid_or_missing(
            &models,
            subagent_provider,
            configured_subagent_model,
        ) {
            subagent_defaults_config.model.as_str()
        } else {
            configured_subagent_model
        };
        let subagent_provider_defaults =
            SubagentProviderDefaults::new(subagent_provider, subagent_model);
        let tool_registry =
            build_builtin_registry_with_cms_mode_subagent_limit_and_provider_defaults(
                &cwd,
                cms_config,
                dangerously_bypass_approvals_and_sandbox,
                active_subagent_limit,
                subagent_provider_defaults.clone(),
            )?;
        let profile_store = UserProfileStore::new(&data_root);
        let user_profile = profile_store.load()?;
        let mcp_servers = load_mcp_servers(&data_root)?;
        let hbse_services =
            HbseServiceRefStore::new(data_root.join("hbse-services.json")).load()?;
        let logger = EventLogger::new(Some(data_root.join("traces").join("tui.jsonl")));
        let parallelism = ParallelismConfig::detect();
        let mut approvals =
            crate::guardrails::ApprovalLedger::new_persisted(data_root.join("approvals.json"))
                .unwrap_or_default();
        if dangerously_bypass_approvals_and_sandbox {
            approvals.clear_pending();
        }
        let tool_executor = ToolExecutor {
            registry: tool_registry.clone(),
            guardrails: GuardrailEngine {
                policy: PermissionPolicy {
                    allow_risky_tools: dangerously_bypass_approvals_and_sandbox,
                    require_human_approval: !dangerously_bypass_approvals_and_sandbox,
                    bypass_approvals_and_sandbox: dangerously_bypass_approvals_and_sandbox,
                    ..PermissionPolicy::default()
                },
                approvals,
            },
            runtime_policy: RuntimePolicy::default(),
            logger: logger.clone(),
        };
        let mut session = SessionState::new(&cwd, tools, skills);
        session.system_prompt = default_system_prompt();
        if let Some(provider) = defaults.get("current_provider").and_then(Value::as_str) {
            session.current_provider = provider.to_string();
        }
        if let Some(model) = defaults.get("current_model").and_then(Value::as_str) {
            session.current_model = model.to_string();
        }
        if let Some(level) = defaults
            .get("current_reasoning_level")
            .and_then(Value::as_str)
        {
            session.current_reasoning_level = Some(level.to_string());
        }
        session.fast_mode = defaults
            .get("fast_mode")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        if let Some(prompt) = defaults.get("system_prompt").and_then(Value::as_str) {
            session.system_prompt = prompt.to_string();
        }
        if let Some(persona) = defaults.get("active_persona_id").and_then(Value::as_str)
            && get_persona_with_root(&data_root, persona)?.is_some()
        {
            session.active_persona_id = Some(persona.to_string());
        }
        if session.active_persona_id.is_some() {
            session.system_prompt = apply_persona_to_system_prompt(
                &session.system_prompt,
                session.active_persona_id.as_deref(),
                &data_root,
            );
        }
        let mut provider_registry = ProviderRegistry::default_catalog()?;
        set_openai_sso_auth_root(&mut provider_registry, &data_root);
        if provider_registry.get(&session.current_provider).is_none() {
            session.current_provider = "demo".to_string();
        }
        if model_known_invalid_or_missing(
            &models,
            &session.current_provider,
            &session.current_model,
        ) {
            if let Some(default) = models.default_for_provider(&session.current_provider) {
                session.current_model = default.name.clone();
                if let Some(context_window) = default.context_window {
                    session.context_limit = context_window;
                }
            } else {
                session.current_model.clear();
            }
        }
        let input_history = session.input_history.clone();
        let mut app = Self {
            session,
            sessions,
            agents,
            config,
            commands: CommandRegistry::with_defaults(),
            provider_registry,
            models,
            cms,
            tool_registry,
            tool_executor,
            profile_store,
            user_profile,
            logger,
            parallelism,
            input: InputState {
                history: input_history,
                ..InputState::default()
            },
            renderer: LayoutRenderer::default(),
            chat_scroll_offset: 0,
            cwd,
            data_root,
            running: true,
            clear_requested: false,
            redraw_requested: false,
            risky_tools_enabled: false,
            dangerously_bypass_approvals_and_sandbox,
            autonomous_mode_enabled: false,
            autonomous_level: 1,
            active_subagent_limit,
            subagent_provider_defaults,
            mcp_servers,
            hbse_services,
            pending_send: None,
            pending_background_jobs: Vec::new(),
            pending_speech_jobs: Vec::new(),
            active_speech_recording: None,
            speech_ptt_key: defaults
                .get("speech_ptt_key")
                .and_then(Value::as_str)
                .map(PushToTalkKey::parse)
                .unwrap_or_else(|| Some(DEFAULT_PTT_KEY.clone())),
            speech_ptt_seconds: defaults
                .get("speech_ptt_seconds")
                .and_then(Value::as_u64)
                .unwrap_or(DEFAULT_PTT_SECONDS)
                .clamp(1, 30),
            pending_stream: None,
            pending_cancel: None,
            pending_run_artifact: None,
            pending_steering: None,
            pending_turn_started_at: None,
            pending_turn_last_activity_at: None,
            pending_editor_action: None,
            command_palette_open: false,
            help_overlay_open: false,
            diff_overlay: None,
            diff_scroll_offset: 0,
            info_overlay: None,
            info_scroll_offset: 0,
            sessions_overlay: None,
            profile_overlay: None,
            approval_selected_index: 0,
            search_open: false,
            search_query: String::new(),
            search_match_index: 0,
            mouse_capture_enabled: true,
            chat_area_x: 0,
            chat_area_y: 0,
            chat_area_width: 0,
            chat_area_height: 0,
            chat_render_scroll: 0,
            chat_rendered_lines: Vec::new(),
            chat_render_cache: ChatTranscriptRenderCache::default(),
            drag_anchor: None,
            drag_current: None,
            autonomy: AutonomyState::default(),
            observed_subagent_transcript_signatures: BTreeMap::new(),
            last_subagent_board_poll: None,
        };
        app.autoload_workspace_session()?;
        app.seed_observed_subagent_transcript_signatures();
        app.rebuild_tooling_for_cms()?;
        let provider = app.session.current_provider.clone();
        let _ = app.refresh_provider_models(&provider);
        Ok(app)
    }

    pub fn send_demo(&mut self, content: &str) -> anyhow::Result<String> {
        let mut runner = ConversationRunner {
            provider: ProviderRouter::from_registry(&self.provider_registry)
                .get(&self.session.current_provider)
                .cloned()
                .ok_or_else(|| {
                    anyhow::anyhow!("Unknown provider: {}", self.session.current_provider)
                })?,
            models: self.models.clone(),
            tools: None,
            tool_executor: None,
            event_sink: None,
            cancel_token: None,
            steering_rx: None,
        };
        let (model_content, skill_trace) = self.prepare_lsl_for_content(content)?;
        let envelope = self.cms.prepare_cached_prompt(
            &model_content,
            self.session.current_provider.clone(),
            self.session.current_model.clone(),
        )?;
        let response = runner.send_with_envelope(&mut self.session, &model_content, envelope)?;
        if let Some(trace) = skill_trace {
            let _ = append_skill_trace(&self.skill_trace_path(), trace);
        }
        let _ = self.cms.complete_turn(content, &response);
        self.autosave_session();
        Ok(response)
    }

    pub fn send_headless(&mut self, content: &str) -> anyhow::Result<String> {
        self.send_headless_streaming(content, &mut |_| {})
    }

    pub fn send_headless_observed(&mut self, content: &str) -> anyhow::Result<HeadlessObservedRun> {
        let events = Arc::new(std::sync::Mutex::new(Vec::new()));
        let captured_events = Arc::clone(&events);
        let mut runner = ConversationRunner {
            provider: ProviderRouter::from_registry(&self.provider_registry)
                .get(&self.session.current_provider)
                .cloned()
                .ok_or_else(|| {
                    anyhow::anyhow!("Unknown provider: {}", self.session.current_provider)
                })?,
            models: self.models.clone(),
            tools: Some(self.tool_registry.clone()),
            tool_executor: Some(self.tool_executor.clone()),
            event_sink: Some(Arc::new(move |event| {
                if let Ok(mut events) = captured_events.lock() {
                    events.push(event);
                }
            })),
            cancel_token: None,
            steering_rx: None,
        };
        let (model_content, skill_trace) = self.prepare_lsl_for_content(content)?;
        let prompt_envelope = self.cms.prepare_cached_prompt(
            &model_content,
            self.session.current_provider.clone(),
            self.session.current_model.clone(),
        )?;
        let response = runner.send_with_envelope_streaming(
            &mut self.session,
            &model_content,
            prompt_envelope.clone(),
            &mut |_| {},
        )?;
        if let Some(trace) = skill_trace {
            let _ = append_skill_trace(&self.skill_trace_path(), trace);
        }
        let (memory_write_results, memory_write_error) =
            match self.cms.complete_turn(content, &response) {
                Ok(results) => (results, None),
                Err(error) => (Vec::new(), Some(error.to_string())),
            };
        self.autosave_session();
        let events = events
            .lock()
            .map(|events| events.clone())
            .unwrap_or_default();
        Ok(HeadlessObservedRun {
            response,
            events,
            prompt_envelope,
            memory_write_results,
            memory_write_error,
        })
    }

    pub fn send_headless_streaming(
        &mut self,
        content: &str,
        on_delta: &mut dyn FnMut(&str),
    ) -> anyhow::Result<String> {
        self.send_headless_streaming_with_event_sink(content, on_delta, None)
    }

    fn send_headless_streaming_with_event_sink(
        &mut self,
        content: &str,
        on_delta: &mut dyn FnMut(&str),
        event_sink: Option<Arc<dyn Fn(ProviderRunEvent) + Send + Sync>>,
    ) -> anyhow::Result<String> {
        let mut runner = ConversationRunner {
            provider: ProviderRouter::from_registry(&self.provider_registry)
                .get(&self.session.current_provider)
                .cloned()
                .ok_or_else(|| {
                    anyhow::anyhow!("Unknown provider: {}", self.session.current_provider)
                })?,
            models: self.models.clone(),
            tools: Some(self.tool_registry.clone()),
            tool_executor: Some(self.tool_executor.clone()),
            event_sink,
            cancel_token: None,
            steering_rx: None,
        };
        let (model_content, skill_trace) = self.prepare_lsl_for_content(content)?;
        let envelope = self.cms.prepare_cached_prompt(
            &model_content,
            self.session.current_provider.clone(),
            self.session.current_model.clone(),
        )?;
        let response = runner.send_with_envelope_streaming(
            &mut self.session,
            &model_content,
            envelope,
            on_delta,
        )?;
        if let Some(trace) = skill_trace {
            let _ = append_skill_trace(&self.skill_trace_path(), trace);
        }
        let _ = self.cms.complete_turn(content, &response);
        self.autosave_session();
        Ok(response)
    }

    pub fn current_prompt_envelope(
        &mut self,
        content: &str,
    ) -> anyhow::Result<CachedPromptEnvelope> {
        let (model_content, _) = self.prepare_lsl_for_content(content)?;
        self.cms.prepare_cached_prompt(
            &model_content,
            self.session.current_provider.clone(),
            self.session.current_model.clone(),
        )
    }

    fn input_edit_width(&self) -> usize {
        let width = self
            .renderer
            .viewport
            .map(|(width, _)| width)
            .or_else(|| {
                crossterm::terminal::size()
                    .ok()
                    .map(|(width, _)| width as usize)
            })
            .unwrap_or(80)
            .max(50);
        width.saturating_sub(2).max(1)
    }

    fn push_system_message(&mut self, content: impl Into<String>) {
        self.session.messages.push(ChatMessage {
            role: "system".to_string(),
            content: content.into(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
    }

    pub(crate) fn autosave_session(&self) {
        let _ = self.sessions.save(&self.session);
        self.remember_workspace_session(&self.cwd, &self.session.session_id);
    }

    fn usrl_metadata_for_contracts(&self, contracts: &[String], key: &str) -> Vec<String> {
        let mut values = Vec::new();
        for skill in &self.session.enabled_skills {
            if skill.kind != "usrl_contract"
                && skill.metadata.get("format").and_then(Value::as_str) != Some("usrl")
            {
                continue;
            }
            let skill_contracts = skill
                .metadata
                .get("usrl_contracts")
                .and_then(Value::as_array)
                .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>())
                .unwrap_or_default();
            if !contracts
                .iter()
                .any(|contract| skill_contracts.iter().any(|item| item == contract))
            {
                continue;
            }
            if let Some(items) = skill.metadata.get(key).and_then(Value::as_array) {
                for item in items.iter().filter_map(Value::as_str) {
                    let item = item.to_string();
                    if !values.contains(&item) {
                        values.push(item);
                    }
                }
            }
        }
        values
    }

    fn rebuild_tooling_for_cms(&mut self) -> anyhow::Result<()> {
        let policy = self.tool_executor.guardrails.policy.clone();
        let bypass = policy.bypass_approvals_and_sandbox;
        let mut tool_registry =
            build_builtin_registry_with_cms_mode_subagent_limit_and_provider_defaults(
                &self.cwd,
                self.cms.config.clone(),
                bypass,
                self.active_subagent_limit,
                self.subagent_provider_defaults.clone(),
            )?;
        let mcp_servers = self.active_mcp_servers();
        register_mcp_tools(
            &mut tool_registry,
            &mcp_servers,
            self.tool_executor.runtime_policy.clone(),
        )?;
        self.tool_registry = tool_registry.clone();
        self.tool_executor.registry = tool_registry;
        self.tool_executor.guardrails.policy = policy;
        if bypass {
            self.tool_executor.guardrails.approvals.clear_pending();
        }
        Ok(())
    }

    fn active_mcp_servers(&self) -> Vec<crate::core::McpServerConfig> {
        let Some(agent_id) = &self.session.active_agent_id else {
            return self.mcp_servers.clone();
        };
        let Ok(profile) = self.agents.load(agent_id) else {
            return Vec::new();
        };
        self.mcp_servers
            .iter()
            .filter(|server| profile.enabled_mcp_servers.contains(&server.id))
            .cloned()
            .collect()
    }

    fn resolve_workspace_path(&self, path: &str) -> PathBuf {
        let path = PathBuf::from(path);
        if path.is_absolute() {
            path
        } else {
            self.cwd.join(path)
        }
    }

    fn help(&self) -> String {
        self.commands
            .all()
            .into_iter()
            .map(|cmd| format!("{:<28} {}", cmd.usage, cmd.description))
            .collect::<Vec<_>>()
            .join("\n")
    }

    fn prepare_lsl_for_content(
        &mut self,
        content: &str,
    ) -> anyhow::Result<(String, Option<LslSkillTrace>)> {
        let cfg = self.lsl_runtime_config();
        let (model_content, trace) = prepare_lsl_augmented_content(
            &self.cwd,
            &self.data_root,
            content,
            &self.session,
            &cfg,
        )?;
        Ok((model_content, trace))
    }

    fn openai_sso_status(&self) -> String {
        crate::openai_sso::OpenAISsoAuthStore::new(Some(self.data_root.clone())).status()
    }

    fn openai_sso_login(&self) -> String {
        crate::openai_sso::login(Some(self.data_root.clone()), true, Duration::from_secs(300))
            .unwrap_or_else(|error| error.to_string())
    }

    fn save_global_model_defaults(&self) -> anyhow::Result<()> {
        self.save_config_defaults()
    }

    fn save_config_defaults(&self) -> anyhow::Result<()> {
        let mut data = self.config.load().unwrap_or_default();
        data.insert(
            "current_provider".to_string(),
            json!(self.session.current_provider),
        );
        data.insert(
            "current_model".to_string(),
            json!(self.session.current_model),
        );
        if let Some(level) = &self.session.current_reasoning_level {
            data.insert("current_reasoning_level".to_string(), json!(level));
        } else {
            data.remove("current_reasoning_level");
        }
        data.insert("fast_mode".to_string(), json!(self.session.fast_mode));
        data.insert(
            "system_prompt".to_string(),
            json!(strip_persona_from_system_prompt(
                &self.session.system_prompt
            )),
        );
        if let Some(persona) = &self.session.active_persona_id {
            data.insert("active_persona_id".to_string(), json!(persona));
        } else {
            data.remove("active_persona_id");
        }
        if let Some(key) = &self.speech_ptt_key {
            data.insert("speech_ptt_key".to_string(), json!(key.to_config_string()));
        } else {
            data.insert("speech_ptt_key".to_string(), json!("off"));
        }
        data.insert(
            "speech_ptt_seconds".to_string(),
            json!(self.speech_ptt_seconds),
        );
        self.config.save(&data)
    }
}

#[cfg(test)]
pub(crate) fn terminal_frame(rendered: &str) -> String {
    rendered
        .split('\n')
        .map(|line| format!("{line}\x1b[K"))
        .collect::<Vec<_>>()
        .join("\r\n")
}

pub(crate) fn apply_persona_to_system_prompt(
    base_prompt: &str,
    persona_id: Option<&str>,
    data_root: &Path,
) -> String {
    let base = strip_persona_from_system_prompt(base_prompt);
    let Some(persona_id) = persona_id else {
        return base;
    };
    let Ok(Some(persona)) = get_persona_with_root(data_root, persona_id) else {
        return base;
    };
    let section = render_persona_prompt_section(&persona);
    if base.trim().is_empty() {
        section
    } else {
        format!("{}\n\n{}", base.trim_end(), section)
    }
}

pub(crate) fn strip_persona_from_system_prompt(prompt: &str) -> String {
    for heading in [
        KA_PROMPT_HEADING,
        "# Communication persona",
        "# Communication soul",
    ] {
        let marker = format!("\n\n{heading}\n");
        if let Some(index) = prompt.find(&marker) {
            return prompt[..index].trim_end().to_string();
        }
        let start_marker = format!("{heading}\n");
        if prompt.starts_with(&start_marker) {
            return String::new();
        }
    }
    prompt.to_string()
}

fn default_subagent_config_value() -> serde_json::Value {
    serde_json::from_str::<serde_json::Value>(include_str!("defaults/subagents.json"))
        .unwrap_or_else(|_| serde_json::json!({}))
}

fn default_subagent_provider_defaults() -> SubagentProviderDefaults {
    let parsed = default_subagent_config_value();
    SubagentProviderDefaults::new(
        parsed
            .get("default_provider")
            .and_then(serde_json::Value::as_str)
            .unwrap_or(""),
        parsed
            .get("default_model")
            .and_then(serde_json::Value::as_str)
            .unwrap_or(""),
    )
}

fn default_subagent_active_limit() -> usize {
    default_subagent_config_value()
        .get("active_limit")
        .and_then(serde_json::Value::as_u64)
        .map(|value| value as usize)
        .unwrap_or(DEFAULT_ACTIVE_SUBAGENT_LIMIT)
        .max(1)
}

#[cfg(test)]
mod tests {
    use super::{StreamEvent, TuiApplication};
    use crate::core::{ChatMessage, SessionState};
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
    use std::{
        sync::{Arc, atomic::AtomicBool, mpsc},
        time::Instant,
    };

    #[test]
    fn terminal_frame_returns_carriage_on_each_rendered_line() {
        assert_eq!(
            super::terminal_frame("one\ntwo\nthree"),
            "one\x1b[K\r\ntwo\x1b[K\r\nthree\x1b[K"
        );
    }

    #[test]
    fn openai_sso_defaults_to_gpt_5_4_mini() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let data_root = tmp.path().join("home");
        std::fs::create_dir_all(&data_root)?;
        std::fs::write(
            data_root.join("config.json"),
            serde_json::json!({
                "current_provider": "openai-sso"
            })
            .to_string(),
        )?;

        let app = TuiApplication::with_data_root(tmp.path(), &data_root)?;

        assert_eq!(app.session.current_provider, "openai-sso");
        assert_eq!(app.session.current_model, "gpt-5.4-mini");
        Ok(())
    }

    #[test]
    fn startup_preserves_gpt_5_4_mini_saved_model() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let data_root = tmp.path().join("home");
        std::fs::create_dir_all(&data_root)?;
        std::fs::write(
            data_root.join("config.json"),
            serde_json::json!({
                "current_provider": "openai-sso",
                "current_model": "gpt-5.4-mini"
            })
            .to_string(),
        )?;

        let app = TuiApplication::with_data_root(tmp.path(), &data_root)?;

        assert_eq!(app.session.current_provider, "openai-sso");
        assert_eq!(app.session.current_model, "gpt-5.4-mini");
        Ok(())
    }

    #[test]
    fn startup_preserves_gpt_5_4_mini_subagent_model() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let data_root = tmp.path().join("home");
        std::fs::create_dir_all(&data_root)?;
        std::fs::write(
            data_root.join("config.json"),
            serde_json::json!({
                "subagent_provider": "openai-sso",
                "subagent_model": "gpt-5.4-mini"
            })
            .to_string(),
        )?;

        let app = TuiApplication::with_data_root(tmp.path(), &data_root)?;

        assert_eq!(app.subagent_provider_defaults.provider, "openai-sso");
        assert_eq!(app.subagent_provider_defaults.model, "gpt-5.4-mini");
        Ok(())
    }

    #[test]
    fn workspace_provider_override_preserves_gpt_5_4_mini_model() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        let data_root = tmp.path().join("home");
        std::fs::create_dir_all(&workspace)?;
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let mut index = app.load_workspace_index();
        index.provider_overrides.insert(
            workspace.display().to_string(),
            super::workspace_state::ProviderSelection {
                provider: "openai-sso".to_string(),
                model: "gpt-5.4-mini".to_string(),
                reasoning_level: None,
                fast_mode: false,
            },
        );
        app.save_workspace_index(&index)?;

        app.apply_provider_selection_for_workspace();

        assert_eq!(app.session.current_provider, "openai-sso");
        assert_eq!(app.session.current_model, "gpt-5.4-mini");
        Ok(())
    }

    #[test]
    fn persona_set_updates_system_prompt_and_defaults() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let response =
            app.persona_command(&["set".to_string(), "chaotic_competent".to_string()])?;
        assert!(response.contains("Ka set to chaotic_competent"));
        assert_eq!(
            app.session.active_persona_id.as_deref(),
            Some("chaotic_competent")
        );
        assert!(app.session.system_prompt.contains("# Communication ka"));
        assert!(
            app.session
                .system_prompt
                .contains("Active ka/persona: `chaotic_competent`")
        );
        assert!(
            app.session
                .system_prompt
                .contains("Ka/persona controls delivery style only")
        );

        let defaults = app.config.load()?;
        assert_eq!(
            defaults
                .get("active_persona_id")
                .and_then(serde_json::Value::as_str),
            Some("chaotic_competent")
        );
        assert!(
            !defaults
                .get("system_prompt")
                .and_then(serde_json::Value::as_str)
                .unwrap_or_default()
                .contains("# Communication ka")
        );
        Ok(())
    }

    #[test]
    fn completed_speech_job_submits_transcript() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.pending_speech_jobs.push(std::thread::spawn(|| {
            Ok(crate::speech::SpeechTranscriptionResult {
                transcript: "hello from speech".to_string(),
                recorder: "test recorder".to_string(),
                audio_path: std::path::PathBuf::from("/tmp/test-speech.wav"),
                audio_bytes: 4096,
                kept_audio: false,
            })
        }));
        for _ in 0..20 {
            if app
                .pending_speech_jobs
                .first()
                .is_some_and(|job| job.is_finished())
            {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }

        assert!(app.poll_background_jobs());
        assert!(app.input.buffer.is_empty());
        assert!(
            app.session.messages.iter().any(|message| {
                message.role == "user" && message.content == "hello from speech"
            })
        );
        assert!(app.session.messages.iter().any(|message| {
            message.role == "system" && message.content.contains("transcript submitted")
        }));
        Ok(())
    }

    #[test]
    fn ka_edit_queues_tui_safe_editor_action() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let response = app.persona_command(&["edit".to_string(), "bug_goblin".to_string()])?;
        assert!(response.contains("Opening editor for ka `bug_goblin`"));
        let action = app
            .pending_editor_action
            .as_ref()
            .expect("ka edit queues external editor action");
        assert_eq!(action.kind, crate::app::PendingEditorKind::KaProfile);
        assert_eq!(action.id, "bug_goblin");
        assert!(action.path.ends_with("ka/bug_goblin.json"));
        assert!(action.path.exists());
        Ok(())
    }

    #[test]
    fn mouse_wheel_scrolls_chat_when_command_palette_is_open() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.open_command_palette();
        app.chat_scroll_offset = 0;
        app.input.selected_suggestion = 3;

        app.handle_mouse_event(MouseEvent {
            kind: MouseEventKind::ScrollUp,
            column: 0,
            row: 0,
            modifiers: KeyModifiers::NONE,
        });

        assert_eq!(app.chat_scroll_offset, 3);
        assert_eq!(app.input.selected_suggestion, 3);
        assert!(app.command_palette_open);
        assert!(app.redraw_requested);
        Ok(())
    }

    #[test]
    fn stream_deltas_do_not_force_follow_when_user_scrolled_up() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "existing".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let (tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);
        app.chat_scroll_offset = 7;

        tx.send(StreamEvent::Delta(" delta".to_string()))?;
        app.poll_stream_events();

        assert_eq!(
            app.session.messages.last().unwrap().content,
            "existing delta"
        );
        assert_eq!(app.chat_scroll_offset, 7);
        assert!(app.redraw_requested);
        Ok(())
    }

    #[test]
    fn tool_events_update_live_chat_and_activity() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let (tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);

        tx.send(StreamEvent::Activity(
            "thinking through tool use".to_string(),
        ))?;
        tx.send(StreamEvent::ToolStart {
            name: "write_file".to_string(),
            args: r#"{"path":"src/lib.rs"}"#.to_string(),
        })?;
        tx.send(StreamEvent::ToolEnd {
            name: "write_file".to_string(),
            ok: true,
            summary: "ok: Wrote src/lib.rs".to_string(),
            detail: None,
        })?;
        app.poll_stream_events();

        assert_eq!(app.session.activity, "finished tool write_file");
        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n");
        assert!(transcript.contains("Running tool: write_file"));
        assert!(transcript.contains("Tool finished: write_file"));
        assert!(app.redraw_requested);
        Ok(())
    }

    #[test]
    fn completed_turn_restores_visible_user_message_over_model_context_wrapper()
    -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "that is perfect, no changes are needed then".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let mut completed = app.session.clone();
        let completed_user = completed
            .messages
            .iter_mut()
            .find(|message| message.role == "user")
            .expect("completed user message");
        completed_user.content = "[Vegvisir user profile]\n- Address the user as: Malice\n[/Vegvisir user profile]\n\nUser request:\nthat is perfect, no changes are needed then".to_string();

        app.restore_latest_visible_user_message(&mut completed);

        let user = completed
            .messages
            .iter()
            .find(|message| message.role == "user")
            .expect("user message");
        assert_eq!(user.content, "that is perfect, no changes are needed then");
        assert!(!user.content.contains("[Vegvisir user profile]"));
        assert!(!user.content.contains("User request:"));
        Ok(())
    }

    #[test]
    fn completed_turn_preserves_live_tool_messages() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.push_live_tool_message("Running tool: read_file {\"path\":\"src/lib.rs\"}".to_string());
        app.push_live_tool_message("Tool finished: read_file - ok: read 20 bytes".to_string());

        let mut completed = app.session.clone();
        completed
            .messages
            .retain(|message| message.role != "system");
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "done".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        app.merge_live_tool_messages(&mut completed);

        let transcript = completed
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n");
        assert!(transcript.contains("Running tool: read_file"));
        assert!(transcript.contains("Tool finished: read_file"));
        assert!(completed.messages.last().unwrap().content.contains("done"));
        Ok(())
    }

    #[test]
    fn completed_turn_does_not_reuse_previous_reasoning_trace() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "tell me a story".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "**Thinking trace**\n\nold story response".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "do you like Goblins?".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let mut completed = app.session.clone();
        completed.messages.pop();
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "Yes, in fantasy stories.".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        app.merge_live_reasoning_trace(&mut completed);

        assert_eq!(
            completed.messages.last().unwrap().content,
            "Yes, in fantasy stories."
        );
        Ok(())
    }

    #[test]
    fn completed_turn_keeps_current_reasoning_trace() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "do you like Goblins?".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "**Thinking trace**\n\ncurrent trace\n\n**Answer**\n\nYes.".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let mut completed = app.session.clone();
        completed.messages.pop();
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "Yes.".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        app.merge_live_reasoning_trace(&mut completed);

        assert_eq!(
            completed.messages.last().unwrap().content,
            "**Thinking trace**\n\ncurrent trace\n\n**Answer**\n\nYes."
        );
        Ok(())
    }

    #[test]
    fn completed_turn_does_not_replace_full_response_with_partial_live_trace() -> anyhow::Result<()>
    {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "make a change".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "**Thinking trace**\n\nworking\n\n**Answer**\n\nDone.\n\nSummary:".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let mut completed = app.session.clone();
        completed.messages.pop();
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "**Thinking trace**\n\nworking\n\n**Answer**\n\nDone.\n\nSummary:\n- final line visible".to_string(),
            attachments: Vec::new(),
                    created_at: chrono::Utc::now(),
});

        app.merge_live_reasoning_trace(&mut completed);

        assert!(
            completed
                .messages
                .last()
                .unwrap()
                .content
                .contains("final line visible")
        );
        Ok(())
    }

    #[test]
    fn completed_turn_appends_final_response_when_live_artifacts_are_longer() -> anyhow::Result<()>
    {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "fix the thing".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "**Code/Diff update from `write_file`**\n\n```diff\n+lots of artifact text\n+that makes the live partial longer\n```\n\nPartial answer".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let mut completed = app.session.clone();
        completed.messages.pop();
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "Final answer that must remain visible.\n\nSummary:\n- completed cleanly"
                .to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        app.merge_live_reasoning_trace(&mut completed);

        let content = &completed.messages.last().unwrap().content;
        assert!(content.contains("Code/Diff update from `write_file`"));
        assert!(content.contains("Final answer that must remain visible"));
        assert!(content.contains("completed cleanly"));
        Ok(())
    }

    #[test]
    fn turn_repair_revives_streaming_session_without_worker() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "fix the stuck thing".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let (_tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);
        app.pending_cancel = Some(Arc::new(AtomicBool::new(false)));
        app.session.status = "streaming".to_string();
        app.session.activity = "stale provider activity".to_string();
        app.redraw_requested = false;

        let report = app.turn_repair(false).expect("dead turn should repair");

        assert!(report.contains("revived dead turn"));
        assert!(app.pending_stream.is_none());
        assert!(app.pending_cancel.is_none());
        assert_eq!(app.session.status, "ready");
        assert!(app.session.activity.is_empty());
        assert!(app.redraw_requested);
        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n");
        assert!(
            transcript.contains("Turn failed before the model produced a normal final summary")
        );
        assert!(transcript.contains("turn_repair detected a dead turn"));
        assert!(
            !app.session
                .messages
                .iter()
                .any(|message| message.role == "assistant" && message.content.is_empty())
        );
        Ok(())
    }

    #[test]
    fn turn_repair_finalizes_finished_worker() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let mut completed = app.session.clone();
        completed.status = "ready".to_string();
        completed.activity.clear();
        completed.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "hello".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        completed.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "done".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.pending_send = Some(std::thread::spawn(move || Ok(completed)));
        app.session.status = "streaming".to_string();

        for _ in 0..20 {
            if app
                .pending_send
                .as_ref()
                .is_some_and(|handle| handle.is_finished())
            {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        let report = app
            .turn_repair(false)
            .expect("finished worker should finalize");

        assert!(report.contains("finalized a completed provider worker"));
        assert!(app.pending_send.is_none());
        assert_eq!(app.session.status, "ready");
        assert!(
            app.session
                .messages
                .iter()
                .any(|message| { message.role == "assistant" && message.content == "done" })
        );
        Ok(())
    }

    #[test]
    fn failed_worker_poll_fires_automatic_turn_recovery() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.start_tui_turn_artifact("recover this failed turn");
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "recover this failed turn".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.status = "streaming".to_string();
        app.session.activity = "provider running".to_string();
        app.pending_send = Some(std::thread::spawn(|| -> anyhow::Result<SessionState> {
            Err(anyhow::anyhow!(
                "simulated provider failure for auto recovery"
            ))
        }));
        app.pending_turn_started_at = Some(Instant::now());
        app.pending_turn_last_activity_at = Some(Instant::now());

        for _ in 0..20 {
            if app
                .pending_send
                .as_ref()
                .is_some_and(|handle| handle.is_finished())
            {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }

        assert!(app.poll_pending_send());

        assert!(app.pending_send.is_none());
        assert_eq!(app.session.status, "ready");
        assert!(app.session.activity.is_empty());
        assert_eq!(
            app.info_overlay
                .as_ref()
                .map(|overlay| overlay.title.as_str()),
            Some("turn failure")
        );
        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n---\n");
        assert!(
            transcript.contains("Turn failed before the model produced a normal final summary")
        );
        assert!(transcript.contains("simulated provider failure for auto recovery"));
        assert!(
            transcript.contains("Automatic turn recovery fired: provider worker returned an error")
        );
        assert!(transcript.contains("Use `/recover last` or `/runs replay-plan"));
        let latest_run = app
            .latest_run_dir()?
            .expect("failed run artifact should exist");
        assert!(latest_run.join("failure.json").exists());
        Ok(())
    }

    #[test]
    fn turn_repair_force_cancels_and_revives_inflight_worker() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "long task".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "partial output".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let cancel = Arc::new(AtomicBool::new(false));
        let worker_cancel = Arc::clone(&cancel);
        app.pending_send = Some(std::thread::spawn(move || {
            while !worker_cancel.load(std::sync::atomic::Ordering::SeqCst) {
                std::thread::sleep(std::time::Duration::from_millis(5));
            }
            Err(anyhow::anyhow!("cancel observed"))
        }));
        app.pending_cancel = Some(Arc::clone(&cancel));
        app.pending_turn_started_at = Some(Instant::now());
        app.pending_turn_last_activity_at = Some(Instant::now());
        app.session.status = "streaming".to_string();

        let report = app.turn_repair(true).expect("forced repair should revive");

        assert!(report.contains("revived stuck in-flight turn"));
        assert!(cancel.load(std::sync::atomic::Ordering::SeqCst));
        assert!(app.pending_send.is_none());
        assert_eq!(app.session.status, "ready");
        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n");
        assert!(transcript.contains("partial output"));
        assert!(transcript.contains("turn_repair was forced"));
        Ok(())
    }

    #[test]
    fn final_exit_drain_preserves_pending_stream_delta_and_requests_draw() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let (tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);
        app.redraw_requested = false;

        tx.send(StreamEvent::Delta("final answer before exit".to_string()))?;
        app.running = false;
        app.drain_before_terminal_exit();

        assert!(
            app.session
                .messages
                .last()
                .unwrap()
                .content
                .contains("final answer before exit")
        );
        assert!(app.should_draw_frame());
        Ok(())
    }

    #[test]
    fn activity_pulse_animates_each_streaming_tick() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.status = "streaming".to_string();
        app.session.activity = "thinking".to_string();
        app.redraw_requested = false;

        app.pulse_activity();
        assert!(app.redraw_requested);
        app.redraw_requested = false;
        app.pulse_activity();
        assert!(app.redraw_requested);
        Ok(())
    }

    #[test]
    fn dangerous_bypass_startup_clears_persisted_pending_approvals() -> anyhow::Result<()> {
        let temp = tempfile::tempdir()?;
        let cwd = temp.path().join("workspace");
        std::fs::create_dir_all(&cwd)?;
        let data_root = temp.path().join("data");
        std::fs::create_dir_all(&data_root)?;

        let mut args = serde_json::Map::new();
        args.insert(
            "path".to_string(),
            serde_json::Value::String("x".to_string()),
        );
        let request = crate::guardrails::ApprovalRequest {
            id: "stale-approval".to_string(),
            tool_name: "write_file".to_string(),
            args,
            reason: "stale approval from a prior non-yolo session".to_string(),
            risk_label: "risky".to_string(),
        };
        let mut ledger =
            crate::guardrails::ApprovalLedger::new_persisted(data_root.join("approvals.json"))?;
        ledger.enqueue(request);
        assert_eq!(ledger.pending_len(), 1);

        let app = TuiApplication::with_data_root_and_dangerous_bypass(&cwd, &data_root, true)?;

        assert!(app.dangerously_bypass_approvals_and_sandbox);
        assert!(
            app.tool_executor
                .guardrails
                .policy
                .bypass_approvals_and_sandbox
        );
        assert!(!app.tool_executor.guardrails.policy.require_human_approval);
        assert_eq!(app.tool_executor.guardrails.approvals.pending_len(), 0);
        Ok(())
    }

    #[test]
    fn rebuild_tooling_preserves_tool_policy() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;

        app.tool_executor.guardrails.policy.allow_risky_tools = true;
        app.tool_executor.guardrails.policy.require_human_approval = false;
        app.tool_executor
            .guardrails
            .policy
            .allowed_commands
            .insert("make".to_string());

        app.rebuild_tooling_for_cms()?;

        assert!(app.tool_executor.guardrails.policy.allow_risky_tools);
        assert!(!app.tool_executor.guardrails.policy.require_human_approval);
        assert!(
            app.tool_executor
                .guardrails
                .policy
                .allowed_commands
                .contains("make")
        );
        assert!(
            !app.tool_executor
                .guardrails
                .policy
                .bypass_approvals_and_sandbox
        );
        Ok(())
    }

    #[test]
    fn successful_tool_code_detail_is_visible_in_assistant_chat() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let (tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        tx.send(StreamEvent::ToolEnd {
            name: "write_file".to_string(),
            ok: true,
            summary: "ok: Wrote src/lib.rs".to_string(),
            detail: Some(
                "```diff
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1 @@
-old
+new
```"
                .to_string(),
            ),
        })?;
        app.poll_stream_events();

        let assistant = app
            .session
            .messages
            .iter()
            .rev()
            .find(|message| message.role == "assistant")
            .expect("assistant message");
        assert!(
            assistant
                .content
                .contains("Code/Diff update from `write_file`")
        );
        assert!(assistant.content.contains("```diff"));
        assert!(assistant.content.contains("+new"));

        let system = app
            .session
            .messages
            .iter()
            .find(|message| message.role == "system")
            .expect("tool log message");
        assert!(system.content.contains("Tool finished: write_file"));
        Ok(())
    }

    #[test]
    fn failed_worker_final_drain_preserves_tool_failure_context() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let (tx, rx) = mpsc::channel();
        app.pending_stream = Some(rx);
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: String::new(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        tx.send(StreamEvent::ToolStart {
            name: "run_command".to_string(),
            args: r#"{"command":["false"]}"#.to_string(),
        })?;
        tx.send(StreamEvent::ToolEnd {
            name: "run_command".to_string(),
            ok: false,
            summary: "ToolError: command exited with status 1".to_string(),
            detail: Some("stderr: simulated failure".to_string()),
        })?;
        drop(tx);

        // This mirrors poll_pending_send's worker-error path: drain final stream
        // events before clearing pending_stream and add an assistant-facing
        // recovery summary. The previous behavior cleared pending_stream first,
        // losing the ToolEnd detail and making the turn look silently truncated.
        app.poll_stream_events();
        app.pending_stream = None;
        app.pop_empty_assistant_placeholder();
        app.push_turn_failure_summary("simulated provider abort".to_string());

        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join(
                "
---
",
            );
        assert!(transcript.contains("Running tool: run_command"));
        assert!(transcript.contains("Tool failed: run_command"));
        assert!(transcript.contains("stderr: simulated failure"));
        assert!(
            transcript.contains("Turn failed before the model produced a normal final summary")
        );
        assert!(transcript.contains("Exact error message:"));
        assert!(transcript.contains("Recent tool/progress events:"));
        assert!(transcript.contains("simulated provider abort"));
        let overlay = app
            .info_overlay
            .as_ref()
            .expect("turn failure should open an inspect overlay");
        assert_eq!(overlay.title, "turn failure");
        assert!(overlay.body.contains("Exact error message:"));
        assert!(overlay.body.contains("simulated provider abort"));
        assert!(app.session.messages.iter().any(|message| {
            message.role == "system"
                && message
                    .content
                    .contains("Turn failed before the model produced a normal final summary")
        }));

        app.push_turn_failure_summary("simulated provider abort again".to_string());
        let latest_failure_summary = app
            .session
            .messages
            .iter()
            .rev()
            .find(|message| {
                message.role == "system"
                    && message.content.contains("simulated provider abort again")
            })
            .expect("latest failure summary");
        assert!(
            !latest_failure_summary
                .content
                .contains("- Turn failed before the model produced a normal final summary")
        );
        Ok(())
    }

    #[test]
    fn session_summary_command_generates_and_saves_file() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let mut app = TuiApplication::with_data_root(&workspace, tmp.path().join("home"))?;
        app.session.title = "Summary Test".to_string();
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "Please fix the thing".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "Fixed it and cargo check passed.".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let response = app.execute_command("/summary --file summary.md")?.unwrap();
        assert!(response.contains("# Session Summary"));
        assert!(response.contains("Saved summary to"));
        let saved = std::fs::read_to_string(workspace.join("summary.md"))?;
        assert!(saved.contains("Summary Test"));
        assert!(saved.contains("Please fix the thing"));
        assert!(saved.contains("cargo check passed"));
        Ok(())
    }

    #[test]
    fn handoff_command_generates_handoff_summary() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let mut app = TuiApplication::with_data_root(&workspace, tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "Continue the Ghidra headless work".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let response = app.execute_command("/handoff")?.unwrap();
        assert!(response.contains("# Agent Handoff Summary"));
        assert!(response.contains("Known Open Items / Next Exact Steps"));
        assert!(response.contains("Handoff Notes for Next Agent"));
        Ok(())
    }

    #[test]
    fn chat_search_opens_filters_and_jumps_matches() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "first parser note".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "second auth note".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.session.messages.push(ChatMessage {
            role: "assistant".to_string(),
            content: "third parser result".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        app.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL));
        assert!(app.search_open);
        for ch in "parser".chars() {
            app.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
        }
        assert_eq!(app.search_query, "parser");
        assert_eq!(app.search_matches(), vec![0, 2]);
        assert_eq!(app.search_match_index, 0);
        let first_offset = app.chat_scroll_offset;

        app.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
        assert_eq!(app.search_match_index, 1);
        assert!(app.chat_scroll_offset < first_offset);
        app.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
        assert!(!app.search_open);
        Ok(())
    }

    #[test]
    fn subagent_board_poll_is_throttled_when_nothing_changed() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        app.observed_subagent_transcript_signatures
            .insert("existing".to_string(), "signature".to_string());

        assert!(!app.poll_subagent_transcript_updates());
        let first_poll = app.last_subagent_board_poll;
        assert!(first_poll.is_some());
        assert!(!app.poll_subagent_transcript_updates());
        assert_eq!(app.last_subagent_board_poll, first_poll);
        Ok(())
    }

    #[test]
    fn idle_streaming_pulse_does_not_force_redraw_without_activity_text() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.status = "streaming".to_string();
        app.session.activity.clear();
        app.redraw_requested = false;

        app.pulse_activity();

        assert_eq!(app.session.activity_tick, 1);
        assert!(!app.redraw_requested);
        Ok(())
    }

    #[test]
    fn subagent_board_updates_are_appended_to_transcript() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let record = crate::subagents::SubAgentTaskRecord {
            id: "subagent-1".to_string(),
            name: "reviewer".to_string(),
            workspace: workspace.clone(),
            goal: "inspect transcript logging".to_string(),
            parent_run_id: None,
            child_run_id: None,
            artifact_dir: None,
            ownership: None,
            provider: None,
            model: None,
            file_scope: vec![workspace.join("vegvisir/src")],
            work_budget: crate::subagents::SubAgentWorkBudget {
                max_steps: Some(2),
                max_tool_calls: Some(3),
                max_read_bytes: Some(4096),
                max_output_bytes: Some(2048),
                allowed_tools: vec!["read_file".to_string()],
                notes: "small excerpts only".to_string(),
            },
            status: crate::subagents::SubAgentStatus::Completed,
            created_at: chrono::Utc::now(),
            started_at: Some(chrono::Utc::now()),
            finished_at: Some(chrono::Utc::now()),
            checkpoint: None,
            final_answer: Some("child trace and final answer".to_string()),
            error: None,
            observability: crate::subagents::SubAgentObservability {
                launch_argv: vec!["vegvisir".to_string(), "run".to_string()],
                launch_env_keys: vec!["VEGVISIR_MAX_TOOL_ROUNDS".to_string()],
                events: vec![crate::subagents::SubAgentObservedEvent {
                    kind: crate::subagents::SubAgentObservedEventKind::ToolStart,
                    name: Some("write_file".to_string()),
                    args: Some("{\"path\":\"src/lib.rs\"}".to_string()),
                    ok: None,
                    summary: None,
                    detail: None,
                }],
                file_changes: vec![crate::subagents::SubAgentFileChange {
                    path: std::path::PathBuf::from("src/lib.rs"),
                    change: crate::subagents::SubAgentFileChangeKind::Modified,
                    before_bytes: Some(4),
                    after_bytes: Some(4),
                    diff: Some(
                        "diff --git a/src/lib.rs b/src/lib.rs\n--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1,1 +1,1 @@\n-old\n+new\n"
                            .to_string(),
                    ),
                }],
                notes: vec!["snapshot ok".to_string()],
            },
        };
        std::fs::write(
            app.subagent_board_path(),
            serde_json::to_string_pretty(&vec![record])?,
        )?;

        assert!(app.poll_background_jobs());
        let transcript = app
            .session
            .messages
            .iter()
            .map(|message| message.content.as_str())
            .collect::<Vec<_>>()
            .join("\n");
        assert!(transcript.contains("Subagent transcript update: Completed"));
        assert!(transcript.contains("subagent-1"));
        assert!(transcript.contains("inspect transcript logging"));
        assert!(transcript.contains("child trace and final answer"));
        assert!(transcript.contains("Subagent launch argv:"));
        assert!(transcript.contains("VEGVISIR_MAX_TOOL_ROUNDS"));
        assert!(transcript.contains("Subagent observed actions:"));
        assert!(transcript.contains("write_file"));
        assert!(transcript.contains("Subagent file changes:"));
        assert!(transcript.contains("Modified: src/lib.rs"));
        assert!(transcript.contains("```diff"));
        assert!(transcript.contains("+new"));
        assert!(transcript.contains("snapshot ok"));
        Ok(())
    }

    #[test]
    fn chat_drag_selection_extracts_rendered_text() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.chat_area_x = 2;
        app.chat_area_y = 3;
        app.chat_area_height = 4;
        app.chat_render_scroll = 10;
        app.chat_rendered_lines = vec![
            String::new(),
            String::new(),
            "  hello selectable world".to_string(),
        ];

        let selected = app.extract_chat_drag_selection((10, 5), (20, 5));

        assert_eq!(selected, "selectable");
        Ok(())
    }

    #[test]
    fn f12_toggles_mouse_capture_mode() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        assert!(app.mouse_capture_enabled);

        app.handle_key_event(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE));
        assert!(!app.mouse_capture_enabled);
        assert!(
            app.session
                .messages
                .last()
                .unwrap()
                .content
                .contains("Mouse capture OFF")
        );

        app.handle_key_event(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE));
        assert!(app.mouse_capture_enabled);
        Ok(())
    }

    #[test]
    fn sessions_submit_opens_selectable_workspace_overlay_without_chat_pollution()
    -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        let other_workspace = tmp.path().join("other");
        std::fs::create_dir_all(&workspace)?;
        std::fs::create_dir_all(&other_workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let active_id = app.session.session_id.clone();
        app.session.title = "active workspace session".to_string();
        app.sessions.save(&app.session)?;

        let mut workspace_session = app.sessions.create(
            "older workspace session",
            app.session.current_provider.clone(),
            app.session.current_model.clone(),
            app.session.enabled_tools.clone(),
            app.session.enabled_skills.clone(),
        );
        workspace_session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "workspace saved turn".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.sessions.save(&workspace_session)?;

        let mut other_session = app.sessions.create(
            "other workspace session",
            app.session.current_provider.clone(),
            app.session.current_model.clone(),
            app.session.enabled_tools.clone(),
            app.session.enabled_skills.clone(),
        );
        other_session.cwd = other_workspace.display().to_string();
        app.sessions.save(&other_session)?;
        let message_count_before = app.session.messages.len();

        app.input.set_buffer("/sessions");
        app.handle_submit();

        let overlay = app
            .sessions_overlay
            .as_ref()
            .expect("sessions overlay should open");
        assert_eq!(overlay.workspace, workspace.display().to_string());
        assert_eq!(overlay.entries.len(), 2);
        assert!(
            overlay
                .entries
                .iter()
                .any(|entry| entry.session_id == active_id)
        );
        assert!(
            overlay
                .entries
                .iter()
                .any(|entry| entry.session_id == workspace_session.session_id)
        );
        assert!(
            !overlay
                .entries
                .iter()
                .any(|entry| entry.session_id == other_session.session_id)
        );
        assert_eq!(app.session.messages.len(), message_count_before);
        assert!(app.info_overlay.is_none());
        Ok(())
    }

    #[test]
    fn sessions_overlay_arrow_enter_loads_selected_session() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let active_id = app.session.session_id.clone();
        app.sessions.save(&app.session)?;

        let mut target = app.sessions.create(
            "load me",
            app.session.current_provider.clone(),
            app.session.current_model.clone(),
            app.session.enabled_tools.clone(),
            app.session.enabled_skills.clone(),
        );
        target.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "selected session content".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let target_id = target.session_id.clone();
        app.sessions.save(&target)?;

        app.execute_command("/sessions")?;
        let selected = app
            .sessions_overlay
            .as_ref()
            .and_then(|overlay| {
                overlay
                    .entries
                    .iter()
                    .position(|entry| entry.session_id == target_id)
            })
            .expect("target session should be listed");
        app.sessions_overlay.as_mut().unwrap().selected = selected;

        app.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

        assert!(app.sessions_overlay.is_none());
        assert_eq!(app.session.session_id, target_id);
        assert_ne!(app.session.session_id, active_id);
        assert_eq!(app.session.status, "ready");
        assert!(app.session.activity.is_empty());
        assert!(app.session.messages.iter().any(|message| {
            message.role == "user" && message.content == "selected session content"
        }));
        assert!(app.info_overlay.is_some());
        Ok(())
    }

    #[test]
    fn load_session_restores_saved_session_and_normalizes_runtime_state() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let original_id = app.session.session_id.clone();
        app.session.title = "loadable".to_string();
        app.session.status = "streaming".to_string();
        app.session.activity = "old in-flight activity".to_string();
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "saved user message".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        app.sessions.save(&app.session)?;

        app.execute_command("/new throwaway")?;
        assert_ne!(app.session.session_id, original_id);

        let response = app
            .execute_command(&format!("/load {original_id}"))?
            .unwrap();

        assert!(response.contains(&format!("Loaded session {original_id}")));
        assert_eq!(app.session.session_id, original_id);
        assert_eq!(app.session.status, "ready");
        assert!(app.session.activity.is_empty());
        assert_eq!(app.session.cwd, workspace.display().to_string());
        assert_eq!(app.sessions.cwd, workspace);
        assert_eq!(app.session.messages.len(), 1);
        assert_eq!(app.session.messages[0].content, "saved user message");
        assert_eq!(app.chat_scroll_offset, 0);
        assert!(app.redraw_requested);
        Ok(())
    }

    #[test]
    fn load_session_resolves_unique_prefix_and_reports_missing_ids() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let session_id = app.session.session_id.clone();
        app.session.title = "prefix-loadable".to_string();
        app.sessions.save(&app.session)?;

        app.execute_command("/new throwaway")?;
        assert_ne!(app.session.session_id, session_id);

        let prefix = &session_id[..8];
        let response = app.execute_command(&format!("/load {prefix}"))?.unwrap();
        assert!(response.contains(&format!("Loaded session {session_id}")));
        assert_eq!(app.session.session_id, session_id);

        let response = app.execute_command("/load definitely-missing")?.unwrap();
        assert!(response.contains("No saved session matching 'definitely-missing'"));
        assert!(response.contains(&app.sessions.store.root.display().to_string()));
        assert!(response.contains("Use /sessions to list saved session ids."));
        Ok(())
    }

    #[test]
    fn load_session_falls_back_to_default_xdg_session_root() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let custom_data_root = tmp.path().join("workspace-data");
        let default_data_root = tmp.path().join("xdg-data").join("vegvisir");
        let mut source_app = TuiApplication::with_data_root(&workspace, &default_data_root)?;
        let session_id = source_app.session.session_id.clone();
        source_app.session.title = "default-root-loadable".to_string();
        source_app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "saved in default xdg root".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        source_app.sessions.save(&source_app.session)?;
        drop(source_app);

        let previous_xdg = std::env::var_os("XDG_DATA_HOME");
        let previous_hidden_paths = std::env::var_os("VEGVISIR_COMMAND_HIDDEN_PATHS");
        unsafe {
            std::env::set_var("XDG_DATA_HOME", tmp.path().join("xdg-data"));
            std::env::remove_var("VEGVISIR_COMMAND_HIDDEN_PATHS");
        }
        let result = (|| -> anyhow::Result<()> {
            let mut app = TuiApplication::with_data_root(&workspace, &custom_data_root)?;
            assert_ne!(app.sessions.store.root, default_data_root.join("sessions"));

            let response = app
                .execute_command(&format!("/load {session_id}"))?
                .unwrap();

            assert!(response.contains(&format!("Loaded session {session_id}")));
            assert_eq!(app.session.session_id, session_id);
            assert_eq!(app.session.messages.len(), 1);
            assert_eq!(app.session.messages[0].content, "saved in default xdg root");
            Ok(())
        })();
        unsafe {
            match previous_xdg {
                Some(value) => std::env::set_var("XDG_DATA_HOME", value),
                None => std::env::remove_var("XDG_DATA_HOME"),
            }
            match previous_hidden_paths {
                Some(value) => std::env::set_var("VEGVISIR_COMMAND_HIDDEN_PATHS", value),
                None => std::env::remove_var("VEGVISIR_COMMAND_HIDDEN_PATHS"),
            }
        }
        result
    }

    #[test]
    fn load_session_with_missing_saved_workspace_stays_in_active_workspace() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let session_id = app.session.session_id.clone();
        app.session.cwd = tmp.path().join("deleted-workspace").display().to_string();
        app.sessions.save(&app.session)?;

        let response = app
            .execute_command(&format!("/load {session_id}"))?
            .unwrap();

        assert!(response.contains(&format!("Loaded session {session_id}")));
        assert_eq!(app.session.session_id, session_id);
        assert_eq!(app.cwd, workspace);
        assert_eq!(app.sessions.cwd, workspace);
        assert_eq!(app.session.cwd, workspace.display().to_string());
        Ok(())
    }

    #[test]
    fn runs_export_zip_creates_archive_or_actionable_failure() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let workspace = tmp.path().join("workspace");
        std::fs::create_dir_all(&workspace)?;
        let data_root = tmp.path().join("home");
        let mut app = TuiApplication::with_data_root(&workspace, &data_root)?;
        let (manager, mut manifest) = crate::run_artifacts::RunArtifactManager::start_with_run_id(
            &workspace,
            &data_root,
            "test-run-export",
            Option::<&std::path::Path>::None,
            "session-1",
            "demo",
            "demo-model",
            None,
        )?;
        manager.write_result("export me")?;
        manager.finish(&mut manifest, crate::run_artifacts::RunStatus::Completed)?;

        let response = app
            .execute_command("/runs export test-run-export --zip")?
            .unwrap();

        assert!(!response.contains("not enabled"));
        assert!(response.contains("test-run-export"));
        assert!(
            workspace
                .join(".vegvisir")
                .join("exports")
                .join("test-run-export.zip")
                .exists()
                || workspace
                    .join(".vegvisir")
                    .join("exports")
                    .join("test-run-export.tar.gz")
                    .exists()
                || response.contains("Run export failed")
        );
        Ok(())
    }

    #[test]
    fn status_command_reports_session_telemetry() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.input_tokens_used = 123;
        app.session.output_tokens_used = 45;
        app.session.provider_reported_input_tokens = 100;
        app.session.provider_reported_output_tokens = 40;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "hello".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });

        let response = app.execute_command("/session-status")?.unwrap();

        assert!(response.contains("Session status"));
        assert!(response.contains("input_tokens: 123"));
        assert!(response.contains("output_tokens: 45"));
        assert!(response.contains("total_tokens: 168"));
        assert!(response.contains("provider_reported_total_tokens: 140"));
        assert!(app.info_overlay.is_some());
        Ok(())
    }

    #[test]
    fn status_submit_opens_overlay_without_chat_pollution() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "hello".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let message_count_before = app.session.messages.len();

        app.input.set_buffer("/status");
        app.handle_submit();

        let overlay = app
            .info_overlay
            .as_ref()
            .expect("status overlay should open");
        assert_eq!(overlay.title, "status");
        assert!(overlay.body.contains("Session status"));
        assert_eq!(app.session.messages.len(), message_count_before);
        assert!(
            app.session
                .messages
                .iter()
                .all(|message| !message.content.contains("pending_approvals:"))
        );
        Ok(())
    }

    #[test]
    fn agent_and_model_submit_open_overlay_without_chat_pollution() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let profile = crate::core::AgentProfile::new(
            "overlay-agent",
            "Overlay Agent",
            "You are an overlay formatting test agent.",
        )?;
        app.agents.save(&profile)?;
        app.session.messages.push(ChatMessage {
            role: "user".to_string(),
            content: "hello".to_string(),
            attachments: Vec::new(),
            created_at: chrono::Utc::now(),
        });
        let message_count_before = app.session.messages.len();

        app.input.set_buffer("/agent show overlay-agent");
        app.handle_submit();
        let overlay = app
            .info_overlay
            .as_ref()
            .expect("agent show overlay should open");
        assert_eq!(overlay.title, "agents");
        assert!(overlay.body.contains("# Agent: Overlay Agent"));
        assert!(overlay.body.contains("```text"));
        assert_eq!(app.session.messages.len(), message_count_before);

        app.input.set_buffer("/model");
        app.handle_submit();
        let overlay = app
            .info_overlay
            .as_ref()
            .expect("model overlay should open");
        assert_eq!(overlay.title, "model");
        assert!(overlay.body.contains("Current model:"));
        assert_eq!(app.session.messages.len(), message_count_before);
        Ok(())
    }

    #[test]
    fn autonomous_command_toggles_runtime_mode() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        assert!(!app.autonomous_mode_enabled);

        let response = app.execute_command("/auto on")?.unwrap();
        assert!(app.autonomous_mode_enabled);
        assert!(response.contains("Autonomous working mode enabled"));

        let status = app.execute_command("/auto status")?.unwrap();
        assert!(status.contains("Autonomous working mode: enabled"));

        let response = app.execute_command("/auto off")?.unwrap();
        assert!(!app.autonomous_mode_enabled);
        assert!(response.contains("disabled"));

        let response = app.execute_command("/autonomous on")?.unwrap();
        assert!(app.autonomous_mode_enabled);
        assert!(response.contains("Autonomous working mode enabled"));
        Ok(())
    }

    #[test]
    fn autonomous_contract_wraps_user_task_for_model_turn() {
        let wrapped = crate::app::runtime::apply_autonomous_mode_contract("build the feature", 3);
        assert!(wrapped.contains("Vegvisir autonomous working mode is ENABLED"));
        assert!(wrapped.contains("unattended project-work mode"));
        assert!(wrapped.contains("User task:\nbuild the feature"));
        assert!(wrapped.contains("Stop and request approval"));
    }

    #[test]
    fn user_profile_context_wraps_user_task_for_model_turn() {
        let profile =
            "[Vegvisir user profile]\n- Address the user as: Malice\n[/Vegvisir user profile]";
        let wrapped =
            crate::app::runtime::apply_user_profile_context(Some(profile), "build the feature");
        assert!(wrapped.contains("Address the user as: Malice"));
        assert!(wrapped.contains("User request:\nbuild the feature"));
    }

    #[test]
    fn subagent_delegation_context_wraps_user_task_for_model_turn() {
        let wrapped = crate::app::runtime::apply_subagent_delegation_context(
            "inspect provider and renderer behavior",
        );
        assert!(wrapped.contains("Vegvisir subagent delegation policy"));
        assert!(wrapped.contains("proactively delegate"));
        assert!(wrapped.contains("spawn_subagent"));
        assert!(wrapped.contains("Do not spawn subagents for trivial"));
        assert!(wrapped.contains("User request:\ninspect provider and renderer behavior"));
    }

    #[test]
    fn spawn_subagent_tool_is_available_without_risky_mode() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let tool = app.tool_registry.get("spawn_subagent")?;

        assert!(!tool.risky);
        assert!(tool.description.contains("Delegate a bounded task"));
        Ok(())
    }

    #[test]
    fn agents_max_updates_active_subagent_limit() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;

        assert_eq!(
            app.active_subagent_limit,
            crate::tools::DEFAULT_ACTIVE_SUBAGENT_LIMIT
        );

        let response = app.execute_command("/agents max=7")?.unwrap();

        assert_eq!(app.active_subagent_limit, 7);
        assert!(response.contains("Active subagent limit set to 7"));
        assert!(response.contains("YOLO mode"));
        Ok(())
    }

    #[test]
    fn subagent_defaults_load_from_default_config_file() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let defaults = super::default_subagent_provider_defaults();

        assert_eq!(app.subagent_provider_defaults, defaults);
        assert_eq!(
            app.active_subagent_limit,
            super::default_subagent_active_limit()
        );
        Ok(())
    }

    #[test]
    fn subagents_config_command_persists_provider_model_and_limit() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;

        let response = app
            .execute_command("/subagents config provider provider-x model model-x max 6")?
            .expect("command response");

        assert!(response.contains("provider=provider-x"));
        assert!(response.contains("model=model-x"));
        assert!(response.contains("active_limit=6"));
        assert_eq!(app.subagent_provider_defaults.provider, "provider-x");
        assert_eq!(app.subagent_provider_defaults.model, "model-x");
        assert_eq!(app.active_subagent_limit, 6);

        let saved = app.config.load()?;
        assert_eq!(
            saved
                .get("subagent_provider")
                .and_then(serde_json::Value::as_str),
            Some("provider-x")
        );
        assert_eq!(
            saved
                .get("subagent_model")
                .and_then(serde_json::Value::as_str),
            Some("model-x")
        );
        assert_eq!(
            saved
                .get("subagent_active_limit")
                .and_then(serde_json::Value::as_u64),
            Some(6)
        );
        Ok(())
    }

    #[test]
    fn subagents_max_updates_active_subagent_limit() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;

        let response = app.execute_command("/subagents max 5")?.unwrap();

        assert_eq!(app.active_subagent_limit, 5);
        assert!(response.contains("Active subagent limit set to 5"));
        let policy = app.execute_command("/subagents policy")?.unwrap();
        assert!(policy.contains("current session limit is 5"));
        Ok(())
    }

    #[test]
    fn agent_prompt_uses_active_profile_without_harness_prompt() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let mut profile = crate::core::AgentProfile::new(
            "reviewer",
            "Reviewer",
            "You are a focused review specialist.",
        )?;
        profile.mode = "review".to_string();

        app.apply_agent_profile(&profile)?;

        assert_eq!(
            app.session.system_prompt,
            "You are a focused review specialist."
        );
        Ok(())
    }

    #[test]
    fn ctrl_c_cancels_in_flight_response_before_quitting() -> anyhow::Result<()> {
        let tmp = tempfile::tempdir()?;
        let mut app = TuiApplication::with_data_root(tmp.path(), tmp.path().join("home"))?;
        let session = app.session.clone();
        app.pending_send = Some(std::thread::spawn(move || Ok(session)));
        app.session.status = "streaming".to_string();
        app.running = true;

        app.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));

        assert!(app.running);
        assert!(app.pending_send.is_none());
        assert_eq!(app.session.status, "ready");
        assert!(
            app.session
                .messages
                .last()
                .is_some_and(|message| message.content.contains("Cancelled in-flight"))
        );

        app.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
        assert!(!app.running);
        Ok(())
    }
}
