//! Tests for the dispatch module tree: shared fixtures and per-domain test modules.
mod auth;
mod billing;
mod dashboard;
mod jump;
mod modes;
mod notes;
mod permissions;
mod prompt;
mod rewind;
mod router;
mod session;
mod settings;
mod status;
mod task_result;
mod transcript;
mod turn;
use super::ctx::{find_agent_by_session_id, get_active_agent, get_active_agent_mut};
use super::dashboard::{
    apply_pending_dispatch_config, dispatch_dashboard_attach, dispatch_dashboard_begin_rename,
    dispatch_dashboard_commit_rename, dispatch_dashboard_confirm_worktree,
    dispatch_dashboard_create_new_agent_with_detail, dispatch_dashboard_dispatch,
    dispatch_dashboard_dispatch_slash, dispatch_dashboard_overlay_cycle,
    dispatch_dashboard_overlay_exit, dispatch_dashboard_overlay_stop,
    dispatch_dashboard_peek_reply, dispatch_dashboard_permission_followup,
    dispatch_dashboard_permission_select, dispatch_dashboard_question_answer,
    dispatch_dashboard_stop, dispatch_dashboard_toggle_auto_approve, dispatch_exit_dashboard,
    dispatch_open_dashboard, ensure_dashboard_state, resolve_location_input,
};
use super::modes::{
    YOLO_ON_UNDER_PLAN_TOAST, active_agent_plan_nudge_state, dispatch_cycle_mode_and_sync,
    permission_mode_toast,
};
use super::permissions::drain_permission_queue;
use super::prompt::{
    dispatch_send_prompt, dispatch_send_prompt_inner, input_can_trigger_project_picker,
};
use super::session::fork::build_child_fork_marker;
use super::session::lifecycle::{dispatch_new_session_inner, drain_startup_actions, finish_trust};
use super::session::load::{dispatch_load_session_with_restore, reanchor_grouped_selection};
use super::session::modal::{dispatch_rename_session, dispatch_sessions_confirm_close};
use super::settings::setters::set_default_model_inner;
use super::settings::ui::{action_for_reset, apply_setting_rollback};
use super::status::scrub_error_for_toast;
use super::task_result::dispatch_task_result;
use super::*;
use crate::acp::model_state::ModelState;
use crate::acp::tracker::AcpUpdateTracker;
use crate::app::actions::{Action, Effect, SubagentKillOutcome, SwitchModelError, TaskResult};
use crate::app::agent::{AgentId, AgentSession, AgentState};
use crate::app::agent_view::{ActivePane, AgentView, PromptMode};
use crate::app::app_view::{ActiveView, AppView, AuthMode, AuthState, TrustState};
use crate::scrollback::block::RenderBlock;
use crate::scrollback::blocks::{SessionEvent, ToolCallBlock};
use crate::scrollback::state::ScrollbackState;
use agent_client_protocol as acp;
use indexmap::IndexMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
fn test_app() -> AppView {
    let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
    AppView {
        active_view: ActiveView::Welcome,
        auth_return_view: None,
        agents: IndexMap::new(),
        next_agent_id: 0,
        models: ModelState::default(),
        registry: crate::actions::ActionRegistry::defaults(),
        settings_registry: std::sync::Arc::new(crate::settings::SettingsRegistry::defaults()),
        current_ui: kigi_shell::agent::config::UiConfig::default(),
        cwd: PathBuf::from("/tmp"),
        project_picker_shown: true,
        project_picker_disabled: false,
        cwd_has_git_ancestor: false,
        acp_tx: tx,
        scratch: crate::scrollback::render::ScratchBuffer::new(),
        cursor: crate::render::draw::CursorState::new(),
        pending_action: None,
        exit_session_pending: None,
        scroll_state: crate::input::mouse::MouseScrollState::default(),
        scroll_config: crate::input::mouse::ScrollConfig::default(),
        appearance: crate::appearance::AppearanceConfig::default(),
        notification_service: crate::notifications::NotificationService::new(Default::default()),
        pending_notification_escapes: None,
        deferred_notification: None,
        tracing_rx: None,
        tips: Vec::new(),
        tip: None,
        cli_model_override: None,
        cli_effort_token: None,
        default_yolo: false,
        permission_mode_from_soft_default: true,
        auto_mode_gate: true,
        yolo_policy_block: None,
        yolo_launch_block_notice: None,
        screen_mode_switch_hint: None,
        require_plan_approval: false,
        plan_mode: false,
        chat_mode: false,
        subagents: false,
        ask_user: false,
        mouse_captured: true,
        new_worktree_dialog: None,
        contextual_hints: Default::default(),
        remote_contextual_hints: None,
        tip_seen_counts: Default::default(),
        last_known_terminal_rows: 0,
        small_screen_tip_evaluated: false,
        clipboard_focus_tip: Default::default(),
        new_session_worktree_mode: crate::app::app_view::WorktreeMode::Never,
        fork_worktree_mode: crate::app::app_view::WorktreeMode::Ask,
        restore_code: None,
        agent_override: None,
        bootstrap_acp_commands: Vec::new(),
        auth_methods: vec![acp::AuthMethod::Agent(acp::AuthMethodAgent::new(
            acp::AuthMethodId::new("kimi-code"),
            "Kigi".to_string(),
        ))],
        auth_state: AuthState::Done,
        trust_state: TrustState::Done,
        login_label: None,
        login_method_id: None,
        auth_start_mode: AuthMode::Pending,
        auth_code_input: String::new(),
        next_auth_request_seq: 1,
        deferred_startup: Default::default(),
        auth_use_oauth: false,
        auth_clipboard_copied: false,
        show_tips: None,
        auto_update: None,
        ask_user_question_timeout_enabled: None,
        bundle_state: crate::app::bundle::BundleState::default(),
        scroll_debug_hud: crate::views::scroll_debug_hud::ScrollDebugHud::new(),
        fps_hud: crate::views::fps_hud::FpsHud::new(),
        welcome_prompt: crate::views::prompt_widget::PromptWidget::new(),
        slash_mru: std::rc::Rc::new(std::cell::RefCell::new(
            crate::slash::mru::SlashMru::new_in_memory(),
        )),
        welcome_prompt_focused: false,
        welcome_tip_typing_dismissed: false,
        welcome_menu_index: None,
        welcome_menu_scroll: 0,
        auth_in_flight_method: None,
        welcome_menu_rects: Vec::new(),
        welcome_import_banner_rect: None,
        last_mouse_pos: None,
        last_scroll_pos: None,
        last_cache_evict_at: None,
        welcome_prompt_rect: None,
        welcome_auth_url_rect: None,
        welcome_on_auth_url: false,
        welcome_auth_fallback_rect: None,
        auth_show_raw_url: false,
        auth_mouse_disabled: false,
        session_picker_entries: None,
        session_picker_loading: false,
        session_picker_state: crate::views::picker::PickerState::with_mode(
            crate::views::picker::PickerMode::FullScreen,
        ),
        session_picker_source_filter: crate::views::session_picker::SourceFilter::default(),
        session_picker_content_results: None,
        session_picker_content_loading: false,
        session_picker_deep_search_seq: 0,
        session_picker_list_seq: 0,
        foreign_session_compat: Default::default(),
        foreign_session_scan_seq: 0,
        foreign_scan_coordinator: Default::default(),
        session_picker_lanes: Default::default(),
        session_picker_detail_generation: 0,
        session_picker_entries_query: None,
        welcome_tick: 0,
        welcome_shimmer_frame: 0,
        startup_warnings: Vec::new(),
        is_api_key_auth: false,
        pending_update_version: None,
        foreign_resume_launch_generation: 0,
        foreign_resume_launch: None,
        quit_for_update: false,
        relaunch: None,
        import_claude_modal: None,
        welcome_doc_viewer: None,
        screen_mode: crate::app::ScreenMode::Inline,
        pending_effects: Vec::new(),
        pending_editor_path: None,
        pending_agents_modal_refresh: None,
        pending_pager_path: None,
        pending_pager_ansi: false,
        minimal_state: crate::minimal_api::MinimalState::default(),
        reconnect_pending: false,
        show_resolved_model: true,
        usage_visible: true,
        leader_mode: true,
        leader_roster: Vec::new(),
        dashboard_local_sessions: Vec::new(),
        dashboard_sessions_loading: false,
        shared_prompt_queues: std::collections::HashMap::new(),
        optimistic_prompt_echoes: std::collections::HashMap::new(),
        pending_running_adoptions: std::collections::HashMap::new(),
        session_picker_grouped: false,
        cancel_rewind_enabled: true,
        session_recap_available: false,
        dashboard: None,
        dashboard_persisted: None,
        keyboard_normalizer: crate::input::KeyboardNormalizer::from_terminal_context(),
        has_claude_import: false,
    }
}
/// Build a default `AgentSession` for
/// tests. Centralises the fixture so new fields on `AgentSession`
/// don't break every test that constructs one by hand. The
/// `acp_tx` is cloned from the test `AppView`; the
/// `deferred_model_switch` is pulled from the `AppView`'s CLI
/// overrides for parity with `dispatch_new_session_inner`.
fn make_test_agent_session(app: &AppView, id: AgentId, sid: &str) -> AgentSession {
    AgentSession {
        id,
        acp_tx: app.acp_tx.clone(),
        session_id: Some(sid.to_string().into()),
        models: ModelState::default(),
        state: AgentState::Idle,
        tracker: AcpUpdateTracker::new(),
        cwd: PathBuf::from("/tmp"),
        is_worktree: false,
        forked_from: None,
        pending_prompts: std::collections::VecDeque::new(),
        next_queue_id: 0,
        yolo_mode: false,
        auto_mode: false,
        prompt_history: Vec::new(),
        prompt_history_loading: false,
        loading_replay: false,
        restore_degree: None,
        rate_limited: false,
        model_incompatible: false,
        available_commands: Vec::new(),
        available_commands_generation: 0,
        available_tools: None,
        model_switch_pending: false,
        user_model_preference: None,
        deferred_model_switch: app.deferred_model_switch_from_cli(),
        bg_tasks: std::collections::BTreeMap::new(),
        bg_tool_call_to_task: std::collections::HashMap::new(),
        scheduled_tasks: std::collections::HashMap::new(),
        in_flight_prompt: None,
        current_prompt_id: None,
        created_via_new: false,
    }
}
pub(super) fn test_app_with_agent() -> AppView {
    let mut app = test_app();
    let id = AgentId(0);
    let session = make_test_agent_session(&app, id, "test-session");
    let mut agent = AgentView::new(session, ScrollbackState::new());
    agent.active_pane = ActivePane::Scrollback;
    app.agents.insert(id, agent);
    app.next_agent_id = 1;
    switch_to_agent(&mut app, id, SwitchCause::New);
    app
}
/// Give a test agent a generated title so the dashboard renders it.
///
/// The dashboard hides empty (no-real-turn) sessions
/// (`views::dashboard::row::is_empty_top_level`); nav/render tests that
/// rely on their placeholder agents being visible call this to opt in.
fn mark_agent_nonempty(app: &mut AppView, id: AgentId) {
    if let Some(a) = app.agents.get_mut(&id) {
        a.generated_session_title = Some(format!("Session {}", id.0));
    }
}
/// Push a plain prompt directly onto the LOCAL drip-feed queue
/// (`pending_prompts`), bypassing the server-authoritative
/// immediate-send routing. Used by tests that exercise the local
/// `maybe_drain_queue` / editing / `DrainQueue` machinery, which is still
/// the path for image/skill/bash/editing prompts and idle drains.
pub(super) fn enqueue_local(app: &mut AppView, id: AgentId, text: &str) {
    app.agents
        .get_mut(&id)
        .unwrap()
        .session
        .enqueue_prompt(text.to_string());
}
fn make_test_subagent(child_sid: &str, sa_id: &str) -> crate::app::subagent::SubagentInfo {
    crate::app::subagent::SubagentInfo {
        subagent_id: Arc::from(sa_id),
        child_session_id: Arc::from(child_sid),
        description: Arc::from("test subagent"),
        subagent_type: Arc::from("general-purpose"),
        persona: None,
        role: None,
        model: None,
        context_source: None,
        resumed_from: None,
        capability_mode: None,
        context_normalized: false,
        parent_prompt_id: None,
        started_at: std::time::Instant::now(),
        last_progress_at: std::time::Instant::now(),
        finished: false,
        status: None,
        error: None,
        duration_ms: None,
        tool_calls: None,
        turns: None,
        turn_count: None,
        tool_call_count: None,
        tokens_used: None,
        context_window_tokens: None,
        context_usage_pct: None,
        tools_used: Vec::new(),
        error_count: None,
        activity_label: None,
        is_background: false,
        pending_kill: false,
        kill_requested_at: None,
        scrollback_entry_id: None,
        prompt: None,
        child_cwd: None,
        worktree_path: None,
        child_updates_replayed: false,
    }
}
fn arm_reconcile(
    app: &mut AppView,
    id: AgentId,
    prompt_id: &str,
    stop_reason: &str,
    age: std::time::Duration,
) {
    arm_reconcile_with_trigger(app, id, prompt_id, stop_reason, None, age);
}
/// [`arm_reconcile`] with an explicit `_meta.cancelTrigger`.
fn arm_reconcile_with_trigger(
    app: &mut AppView,
    id: AgentId,
    prompt_id: &str,
    stop_reason: &str,
    cancel_trigger: Option<&str>,
    age: std::time::Duration,
) {
    app.agents.get_mut(&id).unwrap().pending_turn_end_reconcile =
        Some(crate::app::agent_view::PendingTurnEnd {
            prompt_id: prompt_id.into(),
            stop_reason: Some(stop_reason.into()),
            agent_result: None,
            cancel_trigger: cancel_trigger.map(str::to_string),
            received_at: std::time::Instant::now() - age,
        });
}
pub(super) fn end_turn() -> Action {
    Action::TaskComplete(TaskResult::PromptResponse {
        agent_id: AgentId(0),
        result: Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)),
        http_status: None,
        prompt_id: None,
    })
}
/// Plant a Build session under the process `kigi_home()` (OnceLock-cached;
/// do not rely on setting `KIGI_SHARE_DIR` mid-process). Caller must remove `sess_dir`.
fn plant_local_build_session(cwd: &std::path::Path, session_id: &str) -> std::path::PathBuf {
    let home = kigi_shell::util::kigi_home::kigi_home();
    let encoded = kigi_shell::util::kigi_home::encode_cwd_dirname(&cwd.to_string_lossy());
    let sess_dir = home.join("sessions").join(encoded).join(session_id);
    std::fs::create_dir_all(&sess_dir).expect("plant session dir");
    std::fs::write(sess_dir.join("summary.json"), b"{}").expect("plant summary");
    sess_dir
}
/// Extract the in-flight auth request sequence, panicking if the auth
/// state is not `Authenticating`.
fn authenticating_seq(app: &AppView) -> u64 {
    match app.auth_state {
        AuthState::Authenticating { request_seq, .. } => request_seq,
        ref other => panic!("expected Authenticating, got {other:?}"),
    }
}
/// Extract text from the last system message in an agent's scrollback.
fn last_system_text(app: &AppView, id: AgentId) -> String {
    system_text_from_end(app, id, 0)
}
/// Like [`last_system_text`] but takes an offset from the end.
/// `offset = 0` is the last entry, `offset = 1` is second-to-last, etc.
fn system_text_from_end(app: &AppView, id: AgentId, offset: usize) -> String {
    let sb = &app.agents[&id].scrollback;
    let idx = sb.len() - 1 - offset;
    let entry = sb.get(idx).expect("scrollback index out of bounds");
    match &entry.block {
        RenderBlock::System(sys) => sys.text.clone(),
        other => panic!("expected System block at index {idx}, got {other:?}"),
    }
}
/// Insert a placeholder agent at `id` so `switch_to_agent` recognises
/// it (the helper's defensive check uses `app.agents.contains_key`).
/// `session_id` and `active_pane` are populated to mirror the
/// existing `test_app_with_agent` setup; these tests do not read
/// either field.
fn insert_placeholder_agent(app: &mut AppView, id: AgentId) {
    let mut agent = AgentView::new(
        AgentSession {
            id,
            acp_tx: app.acp_tx.clone(),
            session_id: Some("placeholder".into()),
            models: ModelState::default(),
            state: AgentState::Idle,
            tracker: AcpUpdateTracker::new(),
            cwd: PathBuf::from("/tmp"),
            is_worktree: false,
            forked_from: None,
            pending_prompts: std::collections::VecDeque::new(),
            next_queue_id: 0,
            yolo_mode: false,
            auto_mode: false,
            prompt_history: Vec::new(),
            prompt_history_loading: false,
            loading_replay: false,
            restore_degree: None,
            rate_limited: false,
            model_incompatible: false,
            available_commands: Vec::new(),
            available_commands_generation: 0,
            available_tools: None,
            model_switch_pending: false,
            user_model_preference: None,
            deferred_model_switch: None,
            bg_tasks: std::collections::BTreeMap::new(),
            bg_tool_call_to_task: std::collections::HashMap::new(),
            scheduled_tasks: std::collections::HashMap::new(),
            in_flight_prompt: None,
            current_prompt_id: None,
            created_via_new: false,
        },
        ScrollbackState::new(),
    );
    agent.active_pane = ActivePane::Scrollback;
    app.agents.insert(id, agent);
}
/// Build an app with three agents (ids 0, 1, 2) and `active_view` set
/// to agent 0.
fn three_agent_app() -> AppView {
    let mut app = test_app_with_agent();
    insert_placeholder_agent(&mut app, AgentId(1));
    insert_placeholder_agent(&mut app, AgentId(2));
    app
}
use crate::slash::commands::fork::ForkArgs;
fn fork_args(worktree_override: Option<bool>, directive: Option<&str>) -> ForkArgs {
    ForkArgs {
        worktree_override,
        directive: directive.map(String::from),
    }
}
/// Build a single-agent app for the `/fork` dispatcher tests.
///
/// Sets `current_branch` to `Some("main")` so the agent appears to be
/// inside a git repo. This is required because `dispatch_fork` skips
/// the worktree question when `current_branch` is `None` (non-git cwd).
fn fork_test_app() -> AppView {
    let mut app = test_app_with_agent();
    app.agents.get_mut(&AgentId(0)).unwrap().current_branch = Some("main".into());
    app
}
/// Build a minimal `AcpArgs<acp::ExtRequest>` for an
/// `kigi/ask_user_question` ext-method request. Returns the args
/// plus the receiver half of the response oneshot so the test can
/// assert the handler completes the ACP roundtrip.
fn make_ask_user_question_args(
    tool_call_id: &str,
) -> (
    kigi_acp_lib::AcpArgs<acp::ExtRequest>,
    tokio::sync::oneshot::Receiver<kigi_acp_lib::AcpResult<acp::ExtResponse>>,
) {
    use kigi_tools::implementations::kigi::ask_user_question::{
        AskUserQuestionExtRequest, Question, QuestionOption,
    };
    let req = AskUserQuestionExtRequest {
        session_id: "test-session".into(),
        tool_call_id: tool_call_id.into(),
        mode: kigi_tools::implementations::kigi::ask_user_question::AskUserQuestionMode::Default,
        questions: vec![Question {
            question: "ACP-driven question".into(),
            options: vec![QuestionOption {
                label: "ok".into(),
                description: "ok".into(),
                preview: None,
                id: None,
            }],
            multi_select: Some(false),
            id: None,
        }],
    };
    let (tx, rx) = tokio::sync::oneshot::channel();
    let ext = acp::ExtRequest::new(
        "kigi/ask_user_question",
        serde_json::value::to_raw_value(&req)
            .expect("serialize AskUserQuestionExtRequest")
            .into(),
    );
    (
        kigi_acp_lib::AcpArgs {
            request: ext,
            response_tx: tx,
        },
        rx,
    )
}
fn set_forked_from(app: &mut AppView, child: AgentId, parent: AgentId) {
    if let Some(agent) = app.agents.get_mut(&child) {
        agent.session.forked_from = Some(parent);
    }
}
fn make_bg_task(task_id: &str) -> crate::app::agent::BgTaskState {
    crate::app::agent::BgTaskState {
        task_id: task_id.into(),
        tool_call_id: String::new(),
        command: "sleep 99".into(),
        description: None,
        cwd: String::new(),
        output_file: String::new(),
        status: crate::app::agent::BgTaskStatus::Running,
        start_time: std::time::SystemTime::now(),
        end_time: None,
        exit_code: None,
        signal: None,
        stdout: String::new(),
        stdout_line_count: 0,
        truncated: false,
        pending_kill: false,
        kill_requested_at: None,
        scrollback_entry_id: None,
        is_monitor: false,
        restored_from_replay: false,
    }
}
/// Set up a two-agent app: agent 0 is active with "sess-A",
/// agent 1 is inactive with "sess-B" and a bg task.
fn two_agent_app_with_bg_task() -> AppView {
    let mut app = test_app_with_agent();
    app.agents[&AgentId(0)].session.session_id = Some(acp::SessionId::new("sess-A"));
    let id1 = AgentId(1);
    let mut agent1 = AgentView::new(
        AgentSession {
            id: id1,
            acp_tx: app.acp_tx.clone(),
            session_id: Some(acp::SessionId::new("sess-B")),
            models: ModelState::default(),
            state: AgentState::Idle,
            tracker: AcpUpdateTracker::new(),
            cwd: PathBuf::from("/tmp"),
            is_worktree: false,
            forked_from: None,
            pending_prompts: std::collections::VecDeque::new(),
            next_queue_id: 0,
            yolo_mode: false,
            auto_mode: false,
            prompt_history: Vec::new(),
            prompt_history_loading: false,
            loading_replay: false,
            restore_degree: None,
            rate_limited: false,
            model_incompatible: false,
            available_commands: Vec::new(),
            available_commands_generation: 0,
            available_tools: None,
            model_switch_pending: false,
            user_model_preference: None,
            deferred_model_switch: None,
            bg_tasks: std::collections::BTreeMap::new(),
            bg_tool_call_to_task: std::collections::HashMap::new(),
            scheduled_tasks: std::collections::HashMap::new(),
            in_flight_prompt: None,
            current_prompt_id: None,
            created_via_new: false,
        },
        ScrollbackState::new(),
    );
    let mut task = make_bg_task("task-B-1");
    task.pending_kill = true;
    task.kill_requested_at = Some(std::time::Instant::now());
    agent1.session.bg_tasks.insert("task-B-1".into(), task);
    app.agents.insert(id1, agent1);
    app.next_agent_id = 2;
    assert!(matches!(app.active_view, ActiveView::Agent(AgentId(0))));
    app
}
fn project_picker_app() -> AppView {
    let mut app = test_app();
    app.cwd = PathBuf::from("/tmp");
    app.project_picker_shown = false;
    app
}
/// Test helper: open Settings then OpenResetConfirm for `key`.
/// Extracted so individual tests don't have to repeat the
/// open-then-open ritual.
fn setup_reset_confirm_open(app: &mut AppView, key: crate::settings::SettingKey) {
    use crate::views::modal::ActiveModal;
    let _ = dispatch(Action::OpenSettings, app);
    let _ = dispatch(Action::OpenResetConfirm { key }, app);
    let agent = app.agents.get(&AgentId(0)).expect("agent must exist");
    assert!(
        matches!(
            agent.active_modal,
            Some(ActiveModal::ResetSettingsConfirm { .. })
        ),
        "setup_reset_confirm_open: ResetSettingsConfirm must be active",
    );
}
fn make_picker_entry(id: &str, cwd: &str) -> crate::app::app_view::SessionPickerEntry {
    crate::app::app_view::SessionPickerEntry {
        id: id.into(),
        summary: id.into(),
        updated_at: chrono::Utc::now(),
        created_at: chrono::Utc::now(),
        cwd: cwd.into(),
        hostname: None,
        source: "local".into(),
        model_id: None,
        num_messages: 0,
        last_active_at: None,
        branch: None,
        repo_name: "repo".into(),
        worktree_label: None,
        card_detail: None,
    }
}
fn make_conversation_entry(id: &str) -> crate::app::app_view::SessionPickerEntry {
    let mut e = make_picker_entry(id, "");
    e.source = "conversation".into();
    e
}
/// Open a SessionPicker modal on the active agent seeded with `entries`.
fn open_session_picker_with(
    app: &mut AppView,
    entries: Vec<crate::app::app_view::SessionPickerEntry>,
) {
    use crate::views::modal::ActiveModal;
    let agent = get_active_agent_mut(app).expect("active agent");
    agent.active_modal = Some(ActiveModal::SessionPicker {
        state: crate::views::picker::PickerState::default(),
        entries: Some(entries),
        loading: false,
        lanes: Default::default(),
        previous_palette: None,
        window: crate::views::modal_window::ModalWindowState::new(),
        content_results: None,
        content_loading: false,
        deep_search_seq: 0,
        entries_query: None,
        source_filter: crate::views::session_picker::SourceFilter::default(),
        pending_delete: None,
    });
}
/// Toast strings match the expected format and contain on/off
/// status.
fn read_toast(app: &AppView) -> String {
    let agent = app.agents.get(&AgentId(0)).expect("agent must exist");
    agent
        .toast
        .as_ref()
        .map(|(s, _)| s.clone())
        .expect("toast should be set")
}
/// Helper: enqueue a single permission containing the new
/// "enable-always-approve" option (AllowOnce kind, position 0 —
/// default-selected by the real `enqueue_permission` helper),
/// a regular "opt-allow-once" (AllowOnce kind, position 1), and
/// a "opt-reject-once" (RejectOnce, position 2). Mirrors the
/// option list the shell builds for TUI/Pager/Desktop.
/// Returns the response receiver for the injected permission.
fn enqueue_permission_with_enable_always_approve(
    app: &mut AppView,
) -> tokio::sync::oneshot::Receiver<acp::Result<acp::RequestPermissionResponse>> {
    use crate::views::permission_view::{PermissionFocus, PermissionViewState};
    use std::sync::Arc;
    let agent = app.agents.get_mut(&AgentId(0)).unwrap();
    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
    let request = acp::RequestPermissionRequest::new(
        acp::SessionId::new(Arc::from("test-sess")),
        acp::ToolCallUpdate::new(
            acp::ToolCallId::new(Arc::from("tc-enable-aa-1")),
            acp::ToolCallUpdateFields::default(),
        ),
        vec![
            acp::PermissionOption::new(
                acp::PermissionOptionId::new(Arc::from(
                    kigi_workspace::permission::ENABLE_ALWAYS_APPROVE_OPTION_ID,
                )),
                "Yes, and don't ask again for anything",
                acp::PermissionOptionKind::AllowOnce,
            ),
            acp::PermissionOption::new(
                acp::PermissionOptionId::new(Arc::from("opt-allow-once")),
                "Yes, proceed",
                acp::PermissionOptionKind::AllowOnce,
            ),
            acp::PermissionOption::new(
                acp::PermissionOptionId::new(Arc::from("opt-reject-once")),
                "No",
                acp::PermissionOptionKind::RejectOnce,
            ),
        ],
    );
    let options = request.options.clone();
    agent.permission_queue.push_back(PermissionViewState {
        request: kigi_acp_lib::AcpArgs {
            request,
            response_tx,
        },
        id: 1,
        focus: PermissionFocus::Options,
        options,
        active_idx: 0,
        bash_highlights: None,
        bash_selection_count: 0,
        bash_command_raw: None,
        mcp_scope: None,
        title: "test-enable-always-approve".to_string(),
        description: vec![],
        args_expanded: false,
        desc_scroll: 0,
        subagent_label: None,
        options_area_height: 0,
        options_scroll_offset: 0,
    });
    response_rx
}
const POLICY_WARNING: &str = kigi_workspace::permission::resolution::YOLO_PIN_REASON_REQUIREMENTS;
fn agent_toast(app: &AppView) -> Option<String> {
    app.agents[&AgentId(0)]
        .toast
        .as_ref()
        .map(|(s, _)| s.clone())
}
/// Use the `theme_cache::test_lock` to serialize tests that touch
/// the in-memory theme state (single mutable global). Mirrors the
/// pattern used by `theme::cache::tests`.
fn with_theme_test_env(f: impl FnOnce()) {
    let _guard = crate::theme::cache::test_lock()
        .lock()
        .unwrap_or_else(|e| e.into_inner());
    crate::theme::cache::reset_for_test();
    crate::theme::cache::seed_auto_theme_defaults_for_test();
    crate::theme::cache::set(crate::theme::ThemeKind::KigiNight);
    crate::theme::system_appearance::clear_mock();
    f();
    crate::theme::system_appearance::clear_mock();
    crate::theme::cache::reset_for_test();
}
fn agent_scrollback_len(app: &AppView) -> usize {
    app.agents.get(&AgentId(0)).unwrap().scrollback.len()
}
use crate::scrollback::blocks::UserPromptBlock;
/// Helper: open the dashboard against an existing `app`.
fn open_dashboard(app: &mut AppView) {
    let _ = dispatch_open_dashboard(app);
}
/// Display-order list of selectable row ids — the same order
/// `dashboard_neighbor_row` and the renderer walk. Test-only mirror
/// of the row build in `dispatch_dashboard_select`.
fn dashboard_row_order(app: &AppView) -> Vec<crate::views::dashboard::DashboardRowId> {
    let d = app.dashboard.as_ref().unwrap();
    let home = crate::views::dashboard::render::cached_home();
    let roster: &[crate::app::roster::RosterEntry] = if app.leader_mode {
        &app.leader_roster
    } else {
        &app.dashboard_local_sessions
    };
    let rows = crate::views::dashboard::build_rows_with_roster(
        &app.agents,
        &d.pinned,
        &d.reorder,
        None,
        d.grouping,
        &d.filter,
        home,
        roster,
    );
    crate::views::dashboard::render::focusables(
        &rows,
        d.grouping,
        &d.filter,
        &d.collapsed_sections,
        d.idle_show_all,
        d.search_mode,
    )
    .into_iter()
    .filter_map(|f| match f {
        crate::views::dashboard::Focusable::Row(id) => Some(id),
        crate::views::dashboard::Focusable::Section(_)
        | crate::views::dashboard::Focusable::IdleOverflow => None,
    })
    .collect()
}
/// Build a synthetic `PermissionViewState` with the given id and
/// options. Pushes it to the agent's permission_queue.
///
/// Returns the response receiver so tests can verify
/// the response was actually `send`'d through the oneshot. The
/// previous version dropped the receiver (`_rx`), which let
/// "happy-path" tests assert the queue was popped but masked
/// regressions where the pop happened without the corresponding
/// send.
fn push_synthetic_permission(
    agent: &mut crate::app::agent_view::AgentView,
    id: usize,
    options: Vec<(&str, &str)>,
) -> tokio::sync::oneshot::Receiver<Result<acp::RequestPermissionResponse, acp::Error>> {
    use crate::views::permission_view::{PermissionFocus, PermissionViewState};
    let (tx, rx) =
        tokio::sync::oneshot::channel::<Result<acp::RequestPermissionResponse, acp::Error>>();
    let request = kigi_acp_lib::AcpArgs {
        request: acp::RequestPermissionRequest::new(
            acp::SessionId::new(std::sync::Arc::from("sess-1")),
            acp::ToolCallUpdate::new(
                acp::ToolCallId::new(std::sync::Arc::from("tc-1")),
                acp::ToolCallUpdateFields::default(),
            ),
            options
                .iter()
                .map(|(oid, name)| {
                    acp::PermissionOption::new(
                        acp::PermissionOptionId::new(std::sync::Arc::from(*oid)),
                        name.to_string(),
                        if *oid == "reject" {
                            acp::PermissionOptionKind::RejectOnce
                        } else {
                            acp::PermissionOptionKind::AllowOnce
                        },
                    )
                })
                .collect(),
        ),
        response_tx: tx,
    };
    let opts = request.request.options.clone();
    let state = PermissionViewState {
        request,
        id,
        focus: PermissionFocus::Options,
        options: opts,
        active_idx: 0,
        bash_highlights: None,
        bash_selection_count: 0,
        bash_command_raw: None,
        mcp_scope: None,
        title: "Test permission".to_string(),
        description: Vec::new(),
        args_expanded: false,
        desc_scroll: 0,
        subagent_label: None,
        options_area_height: 0,
        options_scroll_offset: 0,
    };
    agent.permission_queue.push_back(state);
    rx
}
const MOUSE_OFF_STICKY: &str = crate::app::MOUSE_OFF_HINT_SCROLLBACK;
fn reset_mouse_capture_enabled(on: bool) {
    crate::app::MOUSE_CAPTURE_ENABLED.store(on, std::sync::atomic::Ordering::Release);
}
fn mouse_capture_is_enabled() -> bool {
    crate::app::MOUSE_CAPTURE_ENABLED.load(std::sync::atomic::Ordering::Acquire)
}
