//! Minimal-mode live region: the small pinned viewport holding the running-turn
//! tail (model B), a one-line status indicator, and the always-focused prompt.
//!
//! Layout (top → bottom): live tail · status · prompt. The tail shows the
//! bottom of the uncommitted run (streaming message / running tool) so output
//! is visible as it generates; finished blocks scroll up into native scrollback
//! via [`super::commit`]. When idle the tail is empty and only status + prompt
//! show.
use kigi_tui::app::PagerTerminal;
use kigi_tui::app::app_view::{ActiveView, AppView};
use kigi_tui::minimal_api;
use kigi_tui::render::Renderable;
use kigi_tui::scrollback::state::ScrollbackState;
use kigi_tui::scrollback::wrappers::EntryRenderer;
use kigi_tui::theme::Theme;
use kigi_tui::views::prompt_widget::PromptStyle;
use kigi_tui::views::turn_status;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, Widget};
/// Left inset (columns) for every auxiliary live-region row: the status row,
/// the info bar, the exit hint, and the todo panel — and the prompt's
/// `chrome_pad_left`.
///
/// Minimal is flush-left: committed/tail blocks zero block pads via
/// [`super::commit::committed_appearance`] and reclaim the accent column via
/// `hide_accent`, so content glyphs (`◆` / `$` / message text) start at column
/// 0, matching the welcome card's outer edge. The prompt and auxiliary rows
/// share that left edge (no chrome pad) so nothing sits ragged against the
/// welcome box.
pub(super) fn live_left_inset(_appearance: &kigi_tui::appearance::AppearanceConfig) -> u16 {
    0
}
/// Shrink `area` from the left by `inset` columns (clamped to the width).
fn inset_left(area: Rect, inset: u16) -> Rect {
    let dx = inset.min(area.width);
    Rect {
        x: area.x + dx,
        width: area.width - dx,
        ..area
    }
}
/// The prompt style used by the minimal live region.
///
/// Shared with [`super::overlay::sync_viewport`] so viewport sizing measures the
/// prompt's height exactly as the live region will draw it.
pub(super) fn prompt_style(appearance: &kigi_tui::appearance::AppearanceConfig) -> PromptStyle {
    PromptStyle {
        focused: true,
        show_prefix: appearance.prompt.show_prefix,
        vpad_top: 0,
        compact: appearance.prompt.compact,
        chrome: true,
        chrome_pad_left: live_left_inset(appearance),
        chrome_pad_right: 0,
        bg_override: Some(Color::Reset),
        accent_color_override: None,
        border_color_override: None,
        prefix_override: None,
        placeholder_override: None,
        show_accent_line: false,
        show_borders: false,
        title: None,
        image_preview: true,
    }
}
/// Draw the pinned live region (tail + status + prompt) into the inline viewport.
pub fn draw_live(app: &mut AppView, terminal: &mut PagerTerminal) {
    let force_todos = minimal_api::minimal_show_todos(app);
    let auth_hint = crate::auth::minimal_auth_hint(&app.auth_state);
    let pending_hint = minimal_pending_hint(&app.pending_action);
    let transcript_hint = if minimal_api::minimal_ctrl_o_opens_transcript(app) {
        "ctrl+o transcript"
    } else {
        "/transcript"
    };
    let transcript_progress = minimal_api::minimal_transcript_progress(app);
    let AppView {
        cursor,
        agents,
        active_view,
        appearance,
        ..
    } = app;
    let agent_id = match active_view {
        ActiveView::Agent(id) => Some(*id),
        _ => None,
    };
    let theme = Theme::current();
    let commit_app = super::commit::committed_appearance(appearance);
    let compact = appearance.prompt.compact;
    let style = prompt_style(appearance);
    let row_inset = live_left_inset(appearance);
    let layout_cfg = &appearance.scrollback.layout;
    let term_h = terminal.last_known_area().height;
    kigi_tui::render::draw::draw_frame(terminal, cursor, |frame, _link_spans| {
        let area = frame.area();
        if area.height == 0 || area.width < 4 {
            return (None, None);
        }
        Clear.render(area, frame.buffer_mut());
        let agent = agent_id.and_then(|id| agents.get_mut(&id));
        let Some(agent) = agent else {
            crate::auth::render_auth(frame.buffer_mut(), area, &theme, &auth_hint);
            return (None, None);
        };
        let status_activity = minimal_advance_phase_timer(agent);
        let show_todos = crate::todo::todo_panel_visible(agent, force_todos);
        let queued = agent.session.pending_prompts.len() + agent.shared_queue.len();
        if let Some(kind) = super::panel::active(agent) {
            let cursor = super::panel::render(frame.buffer_mut(), area, agent, kind, &theme);
            return (cursor, None);
        }
        if super::overlay::app_modal_active(agent) {
            super::overlay::render_app_modal(frame.buffer_mut(), area, agent, compact);
            return (None, None);
        }
        if minimal_api::extensions_modal(agent).is_some() {
            let tick = (now_millis() / 100) as u64;
            if let Some(state) = minimal_api::extensions_modal_mut(agent) {
                kigi_tui::views::extensions_modal::render_extensions_modal(
                    frame.buffer_mut(),
                    area,
                    state,
                    None,
                    compact,
                    tick,
                );
            }
            return (None, None);
        }
        if let Some(modal) = super::overlay::active_modal(agent) {
            let status_h = 1u16.min(area.height);
            let content_w = area.width as usize;
            let modal_h = super::overlay::modal_height(modal, agent, term_h, content_w)
                .min(area.height.saturating_sub(status_h))
                .max(1);
            let tail_h = area.height.saturating_sub(status_h + modal_h);
            let tick = (now_millis() / 100) as u64;
            if tail_h > 0 {
                let turn_running = agent.session.state.is_turn_running();
                draw_tail(
                    frame.buffer_mut(),
                    Rect {
                        x: area.x,
                        y: area.y,
                        width: area.width,
                        height: tail_h,
                    },
                    &agent.scrollback,
                    turn_running,
                    &theme,
                    &commit_app,
                    &agent.session.cwd,
                    tick,
                );
            }
            render_minimal_status(
                frame.buffer_mut(),
                inset_left(
                    Rect {
                        x: area.x,
                        y: area.y + tail_h,
                        width: area.width,
                        height: status_h,
                    },
                    row_inset,
                ),
                agent,
                &status_activity,
                transcript_progress,
                &theme,
            );
            let modal_area = Rect {
                x: area.x,
                y: area.y + tail_h + status_h,
                width: area.width,
                height: modal_h,
            };
            let cursor = super::overlay::render_modal(
                frame.buffer_mut(),
                modal_area,
                modal,
                agent,
                &theme,
                term_h,
            );
            return (cursor, None);
        }
        let status_h = 1u16.min(area.height);
        let overlay_h = super::overlay::overlay_rows(&agent.prompt, area.width)
            .min(area.height.saturating_sub(status_h + 1));
        let info_h = if overlay_h == 0 {
            1u16.min(area.height.saturating_sub(status_h + 1))
        } else {
            0
        };
        let below_h = overlay_h + info_h;
        let avail = area.height.saturating_sub(status_h + below_h);
        let prompt_h = agent
            .prompt
            .desired_height(area.width, &style, false, avail)
            .min(avail)
            .max(1);
        let rest = avail.saturating_sub(prompt_h);
        let todos_cap = if force_todos {
            rest
        } else {
            rest.min(crate::todo::MAX_TODO_ROWS)
        };
        let todo_lines = if show_todos {
            crate::todo::todo_panel_lines(agent, todos_cap, force_todos)
        } else {
            Vec::new()
        };
        let todos_h = (todo_lines.len() as u16).min(rest);
        let tail_h = rest.saturating_sub(todos_h);
        let tick = (now_millis() / 100) as u64;
        if tail_h > 0 {
            let tail_area = Rect {
                x: area.x,
                y: area.y,
                width: area.width,
                height: tail_h,
            };
            let turn_running = agent.session.state.is_turn_running();
            draw_tail(
                frame.buffer_mut(),
                tail_area,
                &agent.scrollback,
                turn_running,
                &theme,
                &commit_app,
                &agent.session.cwd,
                tick,
            );
        }
        if todos_h > 0 {
            crate::todo::render_todo_panel(
                frame.buffer_mut(),
                inset_left(
                    Rect {
                        x: area.x,
                        y: area.y + tail_h,
                        width: area.width,
                        height: todos_h,
                    },
                    row_inset,
                ),
                &theme,
                &todo_lines,
            );
        }
        let status_area = inset_left(
            Rect {
                x: area.x,
                y: area.y + tail_h + todos_h,
                width: area.width,
                height: status_h,
            },
            row_inset,
        );
        render_minimal_status(
            frame.buffer_mut(),
            status_area,
            agent,
            &status_activity,
            transcript_progress,
            &theme,
        );
        let prompt_area = Rect {
            x: area.x,
            y: area.y + tail_h + todos_h + status_h,
            width: area.width,
            height: prompt_h,
        };
        if overlay_h > 0 {
            super::overlay::render(
                frame.buffer_mut(),
                area,
                prompt_area,
                &mut agent.prompt,
                layout_cfg,
                compact,
                &theme,
            );
        } else if info_h > 0 {
            let info_area = inset_left(
                Rect {
                    x: area.x,
                    y: prompt_area.y + prompt_h,
                    width: area.width,
                    height: info_h,
                },
                row_inset,
            );
            if let Some(hint) = &pending_hint {
                render_exit_hint(frame.buffer_mut(), info_area, &theme, hint);
            } else {
                render_prompt_info(
                    frame.buffer_mut(),
                    info_area,
                    agent,
                    queued,
                    transcript_hint,
                    &theme,
                );
            }
        }
        let result = agent
            .prompt
            .draw(frame.buffer_mut(), prompt_area, None, &style, None);
        (
            result.cursor_pos,
            result
                .post_flush_escapes
                .map(kigi_tui::terminal::overlay::PostFlush::from),
        )
    });
}
fn live_tail_renderer<'a>(
    entry: &'a kigi_tui::scrollback::entry::ScrollbackEntry,
    theme: &'a Theme,
    appearance: &kigi_tui::appearance::AppearanceConfig,
    cwd: &'a std::path::Path,
    tick: u64,
) -> EntryRenderer<'a> {
    EntryRenderer::new(entry, theme)
        .with_appearance(appearance.clone())
        .with_cwd(Some(cwd))
        .with_tick(tick)
        .with_flat_background(true)
        .with_hide_accent(true)
}
/// Render the uncommitted tail (entries past the commit frontier), bottom-anchored
/// so the most recent output is always visible; the topmost visible entry is
/// clipped via `with_skip_rows` when the run is taller than the tail area.
///
/// Starts at the shared [`super::commit::scan_frontier`] stop point so it renders
/// exactly the entries [`tail_height`] measured (the viewport was sized to that —
/// any disagreement makes the prompt jump on commit).
#[allow(clippy::too_many_arguments)]
fn draw_tail(
    buf: &mut Buffer,
    area: Rect,
    sb: &ScrollbackState,
    turn_running: bool,
    theme: &Theme,
    appearance: &kigi_tui::appearance::AppearanceConfig,
    cwd: &std::path::Path,
    tick: u64,
) {
    if area.height == 0 {
        return;
    }
    let width = area.width;
    let renderer = |e| live_tail_renderer(e, theme, appearance, cwd, tick);
    let mut entries = Vec::new();
    let mut i = super::commit::scan_frontier(sb, turn_running).tail_start;
    while let Some(e) = sb.get(i) {
        entries.push(e);
        i += 1;
    }
    if entries.is_empty() {
        return;
    }
    let gap = super::commit::MINIMAL_BLOCK_GAP;
    let heights: Vec<u16> = entries
        .iter()
        .map(|e| renderer(*e).desired_height(width))
        .collect();
    let total: u16 = heights
        .iter()
        .fold(0u16, |acc, &h| acc.saturating_add(h).saturating_add(gap));
    let mut skip_top = total.saturating_sub(area.height);
    let mut y = area.y;
    let bottom = area.y + area.height;
    for (e, &content_h) in entries.iter().zip(&heights) {
        let slot_h = content_h.saturating_add(gap);
        if skip_top >= slot_h {
            skip_top -= slot_h;
            continue;
        }
        let slot_skip = skip_top;
        skip_top = 0;
        let entry_skip = slot_skip.min(content_h);
        let visible_content = content_h.saturating_sub(entry_skip);
        if visible_content > 0 {
            let draw_h = visible_content.min(bottom.saturating_sub(y));
            if draw_h == 0 {
                break;
            }
            let rect = Rect {
                x: area.x,
                y,
                width,
                height: draw_h,
            };
            renderer(*e).with_skip_rows(entry_skip).render(rect, buf);
            y += draw_h;
            if y >= bottom {
                break;
            }
        }
        let gap_skipped = slot_skip.saturating_sub(entry_skip);
        let gap_visible = gap
            .saturating_sub(gap_skipped)
            .min(bottom.saturating_sub(y));
        y += gap_visible;
        if y >= bottom {
            break;
        }
    }
}
/// Count idle-surviving "watchers" — running monitors, active scheduled
/// `/loop` tasks, and running (background) subagents — so the shared turn-status
/// widget can show the persistent "watching · N monitors · M loops · K
/// subagents" cue while the agent is idle. Mirrors the full-TUI computation in
/// `AgentView::draw` (which minimal bypasses).
fn minimal_watchers(agent: &kigi_tui::app::agent_view::AgentView) -> turn_status::Watchers {
    turn_status::Watchers {
        monitors: agent
            .session
            .bg_tasks
            .values()
            .filter(|t| t.is_monitor && t.status == kigi_tui::app::agent::BgTaskStatus::Running)
            .count(),
        loops: agent.session.scheduled_tasks.len(),
        subagents: agent
            .subagent_sessions
            .values()
            .filter(|s| s.is_running())
            .count(),
    }
}
/// Resolve the current turn activity and advance the phase timer when it
/// changes. The full TUI runs this inside its own `draw` (reset
/// `activity_started_at` on every phase transition); minimal has a separate
/// draw path, so it must drive the same logic or the phase timer would never
/// reset. Returns the resolved activity for [`render_minimal_status`].
fn minimal_advance_phase_timer(
    agent: &mut kigi_tui::app::agent_view::AgentView,
) -> Option<kigi_tui::acp::tracker::TurnActivity> {
    let activity = minimal_api::resolve_turn_activity(agent);
    if activity.as_ref() != minimal_api::last_activity(agent) {
        agent.activity_started_at = Some(std::time::Instant::now());
        minimal_api::set_last_activity(agent, activity.clone());
    }
    activity
}
/// Render the one-line minimal status indicator above the prompt.
///
/// Reuses the full-TUI [`turn_status::render_turn_status`] widget so minimal
/// surfaces the same rich activity detail (`Run …` / `Thinking…` /
/// `Waiting on subagent…` / `Retrying (attempt N)…` / `Cancelling…`), the
/// per-phase + turn timers, and the idle "watching · …" cue (running monitors /
/// loops / background subagents) — instead of collapsing everything to
/// "working…". Keyboard-only, so the mouse `[stop]` / `[↓]` buttons are
/// suppressed (`None`), and `flat_background` keeps the row transparent like the
/// rest of the live region. When the widget would draw nothing (plain idle, no
/// watchers) a small `minimal · /help` hint is shown instead.
fn render_minimal_status(
    buf: &mut Buffer,
    area: Rect,
    agent: &kigi_tui::app::agent_view::AgentView,
    activity: &Option<kigi_tui::acp::tracker::TurnActivity>,
    transcript_progress: Option<(usize, usize)>,
    theme: &Theme,
) {
    if area.height == 0 || area.width == 0 {
        return;
    }
    if let Some((done, total)) = transcript_progress {
        let style = theme.primary().bg(Color::Reset);
        buf.set_style(area, style);
        buf.set_span(
            area.x,
            area.y,
            &Span::styled(format!("rendering transcript… {done}/{total}"), style),
            area.width,
        );
        return;
    }
    let watchers = minimal_watchers(agent);
    let drain_blocked = minimal_api::drain_blocked(agent);
    if minimal_api::renders_parked(agent)
        || !turn_status::should_show(
            &agent.session.state,
            drain_blocked,
            minimal_api::mcp_init_progress(agent),
            watchers,
        )
    {
        render_idle_hint(buf, area, theme);
        return;
    }
    let is_pending_user_input =
        !agent.permission_queue.is_empty() || minimal_api::question_view(agent).is_some();
    let goal_verifying = agent
        .goal_state
        .as_ref()
        .is_some_and(|g| g.verifying_completion);
    turn_status::render_turn_status(
        buf,
        area,
        &agent.session.state,
        activity,
        agent.turn_elapsed(),
        agent.activity_started_at,
        agent.scrollback.animation_tick(),
        drain_blocked,
        None,
        false,
        agent.context_state.as_ref().map(|c| c.used),
        minimal_api::mcp_init_progress(agent),
        agent.bash_turn,
        is_pending_user_input,
        goal_verifying,
        watchers,
        true,
        minimal_api::held_queue_count(agent),
        minimal_api::held_queue_top_sendable(agent),
    );
}
/// Idle status: `minimal · [/fullscreen to go back ·] /help` (+ auto-set note).
fn render_idle_hint(buf: &mut Buffer, area: Rect, theme: &Theme) {
    let style = theme.dim().bg(Color::Reset);
    buf.set_style(area, style);
    let auto = kigi_tui::app::minimal_auto_set_for_mouse_leak();
    let switch_back = kigi_tui::app::minimal_show_switch_back_to_fullscreen();
    let hint = match (auto, switch_back) {
        (true, true) => {
            "minimal · auto-set on JetBrains/Windows due to JetBrains mouse reporting issues \
             · /fullscreen to go back · /help"
        }
        (true, false) => {
            "minimal · auto-set on JetBrains/Windows due to JetBrains mouse reporting issues · /help"
        }
        (false, true) => "minimal · /fullscreen to go back · /help",
        (false, false) => "minimal · /help",
    };
    buf.set_span(area.x, area.y, &Span::styled(hint, style), area.width);
}
/// Render the one-line info bar directly below the prompt: the selected model,
/// the active session mode (the Shift+Tab cycle: plan / always-approve / auto),
/// context usage (absolute + percentage), an `N queued` count when prompts
/// are waiting behind a running turn, and the full-transcript shortcut hint
/// (`transcript_hint`: "ctrl+o transcript", or "/transcript" where Ctrl+O is
/// the interject chord — Apple Terminal). Mirrors the regular TUI's model
/// label, mode flags, and context bar; the transcript hint stands in for the
/// full TUI's shortcuts bar, which minimal never renders — without it the
/// folded conversation has no visible way back to the full view. The mode flag
/// keeps its accent color so the Shift+Tab cycle — otherwise invisible in
/// minimal mode — is always shown. Drawn only when no menu/dropdown owns the
/// band below the prompt (the caller gates on that). The elapsed-time / token
/// count lives in the turn-status row above the prompt (see
/// [`render_minimal_status`]), so it is not repeated here.
fn render_prompt_info(
    buf: &mut Buffer,
    area: Rect,
    agent: &kigi_tui::app::agent_view::AgentView,
    queued: usize,
    transcript_hint: &str,
    theme: &Theme,
) {
    use kigi_tui::views::context_bar::fmt_tokens;
    let base = theme.primary().bg(Color::Reset);
    let sep = theme.dim().bg(Color::Reset);
    let mut segs: Vec<(String, Style)> = Vec::new();
    if let Some(model) = agent.session.models.current_model_name() {
        let label = match agent.session.models.reasoning_effort {
            Some(eff) => format!("{model} ({eff})"),
            None => model,
        };
        segs.push((label, base));
    }
    let effective_plan =
        minimal_api::plan_mode_pending(agent).unwrap_or(minimal_api::plan_mode_active(agent));
    let mode_flag: Option<(&str, Color)> = if effective_plan {
        Some(("plan", theme.accent_plan))
    } else if agent.session.is_yolo() {
        Some(("always-approve", theme.warning))
    } else if agent.session.is_auto() {
        Some(("auto", theme.accent_system))
    } else {
        None
    };
    if let Some((label, color)) = mode_flag {
        segs.push((label.to_string(), base.fg(color)));
    }
    let used = agent.context_state.as_ref().map(|c| c.used);
    let total = agent
        .context_state
        .as_ref()
        .and_then(|c| (c.total > 0).then_some(c.total))
        .or_else(|| agent.session.models.get_context_window());
    if let (Some(used), Some(total)) = (used, total)
        && total > 0
    {
        let pct = kigi_token_estimation::usage_percentage(used, total);
        segs.push((
            format!("{} / {} ({:.0}%)", fmt_tokens(used), fmt_tokens(total), pct),
            base,
        ));
    }
    if queued > 0 {
        segs.push((format!("{queued} queued"), base));
        segs.push(("/queue".to_string(), base));
    }
    segs.push((transcript_hint.to_string(), base));
    if segs.is_empty() {
        return;
    }
    buf.set_style(area, base);
    let mut spans: Vec<Span<'static>> = Vec::new();
    for (i, (text, style)) in segs.into_iter().enumerate() {
        if i > 0 {
            spans.push(Span::styled(" · ", sep));
        }
        spans.push(Span::styled(text, style));
    }
    buf.set_line(area.x, area.y, &Line::from(spans), area.width);
}
/// The double-press confirmation hint to show under the prompt (e.g. "press
/// Ctrl+q again to quit"), or `None` when nothing is armed / it has expired or
/// is a silent arm (no label). Mirrors the full-TUI shortcuts-bar `PendingHint`,
/// which minimal does not render.
fn minimal_pending_hint(
    pending: &Option<kigi_tui::app::app_view::PendingAction>,
) -> Option<String> {
    let pending = pending.as_ref()?;
    if pending.expired() {
        return None;
    }
    let label = pending.label?;
    Some(format!(
        "press {} again to {label}",
        pending.shortcut.display()
    ))
}
/// Render the one-line double-press confirmation hint under the prompt, in the
/// warning color so it stands out from the model/context info row.
fn render_exit_hint(buf: &mut Buffer, area: Rect, theme: &Theme, hint: &str) {
    let style = Style::default().fg(theme.warning).bg(Color::Reset);
    buf.set_style(area, style);
    buf.set_span(
        area.x,
        area.y,
        &Span::styled(hint.to_string(), style),
        area.width,
    );
}
/// Height (rows) of the tail that will REMAIN after this frame's commit pass —
/// i.e. the entries `commit_active` will NOT consume, from the first
/// non-committable entry (past the scan cursor) onward.
///
/// The overlay host sizes the live viewport to this *post-commit* tail so the
/// prompt sits right after the streaming output (no fixed gap while a turn is
/// "thinking" with nothing streamed yet). Sizing to the post-commit tail
/// (rather than the current tail) is load-bearing: because `sync_viewport` runs
/// just *before* `commit_active`, the viewport is already at its post-commit
/// height when the commit's `insert_before` prints finalized blocks — so it can
/// reposition the correctly-sized viewport to sit directly after them
/// (content-anchored). Sizing to the tall streaming tail instead left the
/// viewport oversized at commit time, and the following collapse stranded the
/// prompt at the top of the screen (the "snaps to top" bug).
pub(super) fn tail_height(
    agent: &kigi_tui::app::agent_view::AgentView,
    width: u16,
    appearance: &kigi_tui::appearance::AppearanceConfig,
) -> u16 {
    let theme = Theme::current();
    let sb = &agent.scrollback;
    let turn_running = agent.session.state.is_turn_running();
    let gap = super::commit::MINIMAL_BLOCK_GAP;
    let mut i = super::commit::scan_frontier(sb, turn_running).tail_start;
    let mut total = 0u16;
    while let Some(e) = sb.get(i) {
        let h =
            live_tail_renderer(e, &theme, appearance, &agent.session.cwd, 0).desired_height(width);
        total = total.saturating_add(h).saturating_add(gap);
        i += 1;
    }
    total
}
fn now_millis() -> u128 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0)
}
#[cfg(test)]
mod tests {
    use super::*;
    fn agent() -> kigi_tui::app::agent_view::AgentView {
        minimal_api::test_agent_view(Some("s1"), std::path::PathBuf::from("/tmp"))
    }
    #[test]
    fn tail_height_uses_owning_session_cwd_for_tool_paths() {
        use kigi_tui::app::agent::AgentState;
        use kigi_tui::scrollback::RenderBlock;
        use kigi_tui::scrollback::entry::ScrollbackEntry;
        use kigi_tui::scrollback::types::DisplayMode;
        let cwd = std::path::PathBuf::from("/alternate/worktree");
        let mut agent = minimal_api::test_agent_view(Some("s1"), cwd.clone());
        agent.session.state = AgentState::TurnRunning;
        let mut entry = ScrollbackEntry::running(RenderBlock::edit(
            "/alternate/worktree/src/components/really_long_file_name.rs",
            None,
        ));
        entry.set_display_mode(DisplayMode::Expanded);
        agent.scrollback.push(entry);
        let appearance = super::super::commit::committed_appearance(
            &kigi_tui::appearance::AppearanceConfig::default(),
        );
        let theme = Theme::current();
        let entry = agent.scrollback.get(0).unwrap();
        let (width, painted_height, visible_accent_height) = (10..=40)
            .find_map(|width| {
                let painted =
                    live_tail_renderer(entry, &theme, &appearance, &cwd, 0).desired_height(width);
                let visible_accent = EntryRenderer::new(entry, &theme)
                    .with_appearance(appearance.clone())
                    .with_cwd(Some(&cwd))
                    .with_tick(0)
                    .with_flat_background(true)
                    .desired_height(width);
                (painted != visible_accent).then_some((width, painted, visible_accent))
            })
            .expect("fixture must wrap differently when the accent column is reclaimed");
        assert_ne!(painted_height, visible_accent_height);
        assert_eq!(
            tail_height(&agent, width, &appearance),
            painted_height.saturating_add(super::super::commit::MINIMAL_BLOCK_GAP)
        );
    }
    #[test]
    fn minimal_status_shows_rich_activity_and_idle_hint() {
        use kigi_tui::acp::tracker::TurnActivity;
        use kigi_tui::app::agent::AgentState;
        let theme = Theme::current();
        let area = Rect::new(0, 0, 60, 1);
        let read = |buf: &Buffer| -> String {
            (0..area.width)
                .filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
                .collect()
        };
        kigi_tui::app::set_minimal_show_switch_back_to_fullscreen_for_test(false);
        let a = agent();
        let mut buf = Buffer::empty(area);
        render_minimal_status(&mut buf, area, &a, &None, None, &theme);
        let idle = read(&buf);
        assert!(idle.contains("/help"), "idle hint: {idle:?}");
        assert!(
            !idle.contains("/fullscreen"),
            "cold start must not show switch-back: {idle:?}"
        );
        kigi_tui::app::set_minimal_show_switch_back_to_fullscreen_for_test(true);
        let mut buf = Buffer::empty(area);
        render_minimal_status(&mut buf, area, &a, &None, None, &theme);
        let switched = read(&buf);
        assert!(
            switched.contains("/fullscreen to go back"),
            "relaunch into minimal must show switch-back: {switched:?}"
        );
        kigi_tui::app::set_minimal_show_switch_back_to_fullscreen_for_test(false);
        let mut a = agent();
        a.session.state = AgentState::TurnRunning;
        let mut buf = Buffer::empty(area);
        render_minimal_status(
            &mut buf,
            area,
            &a,
            &Some(TurnActivity::Responding),
            None,
            &theme,
        );
        let text = read(&buf);
        assert!(text.contains("Responding"), "rich activity: {text:?}");
        let mut buf = Buffer::empty(area);
        render_minimal_status(
            &mut buf,
            area,
            &a,
            &Some(TurnActivity::Retrying {
                attempt: 2,
                max_retries: 3,
                reason: "transient error".to_string(),
            }),
            None,
            &theme,
        );
        assert!(read(&buf).contains("Retrying"), "retry: {:?}", read(&buf));
    }
    #[test]
    fn minimal_status_shows_idle_watching_cue() {
        use kigi_tui::app::agent::AgentState;
        let theme = Theme::current();
        let area = Rect::new(0, 0, 60, 1);
        let read = |buf: &Buffer| -> String {
            (0..area.width)
                .filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
                .collect()
        };
        let mut a = agent();
        a.session.state = AgentState::Idle;
        a.session.scheduled_tasks.insert(
            "loop-1".to_string(),
            kigi_tui::app::agent::ScheduledTaskInfo {
                task_id: "loop-1".to_string(),
                prompt: "do the thing".to_string(),
                human_schedule: "every 5m".to_string(),
                created_at: std::time::Instant::now(),
                next_fire_at: None,
                tag: "loop".to_string(),
            },
        );
        assert_eq!(minimal_watchers(&a).loops, 1);
        let mut buf = Buffer::empty(area);
        render_minimal_status(&mut buf, area, &a, &None, None, &theme);
        let text = read(&buf);
        assert!(text.contains("watching"), "watching cue: {text:?}");
        assert!(!text.contains("/help"), "not the idle hint: {text:?}");
    }
    #[test]
    fn prompt_info_renders_model_context_and_queued() {
        let mut a = agent();
        a.context_state = Some(kigi_shell::session::ContextInfo {
            used: 276_000,
            total: 2_000_000,
            ..Default::default()
        });
        let theme = Theme::current();
        let area = Rect::new(0, 0, 80, 1);
        let mut buf = Buffer::empty(area);
        render_prompt_info(&mut buf, area, &a, 3, "ctrl+o transcript", &theme);
        let text: String = (0..area.width)
            .filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
            .collect();
        assert!(text.contains("276K"), "absolute used tokens: {text:?}");
        assert!(text.contains("2.0M"), "total context window: {text:?}");
        assert!(text.contains('%'), "percentage: {text:?}");
        assert!(text.contains("3 queued"), "queued count: {text:?}");
        assert!(
            text.trim_end().ends_with("ctrl+o transcript"),
            "trailing transcript hint: {text:?}"
        );
    }
    /// Where Ctrl+O is the interject chord (Apple Terminal) the caller passes
    /// the `/transcript` fallback, and the info row advertises that instead.
    #[test]
    fn prompt_info_shows_slash_transcript_fallback_hint() {
        let a = agent();
        let theme = Theme::current();
        let area = Rect::new(0, 0, 80, 1);
        let mut buf = Buffer::empty(area);
        render_prompt_info(&mut buf, area, &a, 0, "/transcript", &theme);
        let text: String = (0..area.width)
            .filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
            .collect();
        assert!(text.contains("/transcript"), "fallback hint: {text:?}");
        assert!(!text.contains("ctrl+o"), "no dead chord: {text:?}");
    }
    #[test]
    fn prompt_info_shows_session_mode_flag() {
        let theme = Theme::current();
        let area = Rect::new(0, 0, 80, 1);
        let read = |buf: &Buffer| -> String {
            (0..area.width)
                .filter_map(|x| buf.cell((x, 0)).map(|c| c.symbol().to_string()))
                .collect()
        };
        let render = |a: &kigi_tui::app::agent_view::AgentView| -> String {
            let mut buf = Buffer::empty(area);
            render_prompt_info(&mut buf, area, a, 0, "ctrl+o transcript", &theme);
            read(&buf)
        };
        let mut a = agent();
        let text = render(&a);
        assert!(!text.contains("plan"), "normal shows no flag: {text:?}");
        assert!(!text.contains("always-approve"), "normal: {text:?}");
        minimal_api::set_plan_mode_pending(&mut a, Some(true));
        assert!(render(&a).contains("plan"), "plan flag: {:?}", render(&a));
        minimal_api::set_plan_mode_pending(&mut a, None);
        minimal_api::set_plan_mode_active(&mut a, false);
        minimal_api::set_yolo_mode_for_test(&mut a.session, true);
        minimal_api::set_auto_mode_for_test(&mut a.session, true);
        let text = render(&a);
        assert!(text.contains("always-approve"), "yolo flag: {text:?}");
        minimal_api::set_yolo_mode_for_test(&mut a.session, false);
        let text = render(&a);
        assert!(text.contains("auto"), "auto flag: {text:?}");
    }
    #[test]
    fn pending_hint_formats_press_again() {
        use crossterm::event::{KeyCode, KeyModifiers};
        use kigi_tui::app::actions::Action;
        use kigi_tui::app::app_view::PendingAction;
        use kigi_tui::input::key::KeyShortcut;
        assert!(minimal_pending_hint(&None).is_none());
        let shortcut = KeyShortcut::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
        let pending = Some(PendingAction::new(Action::Quit, shortcut, "quit"));
        assert_eq!(
            minimal_pending_hint(&pending).as_deref(),
            Some("press Ctrl+q again to quit")
        );
        let silent = Some(PendingAction::with_ttl(
            Action::Quit,
            shortcut,
            None,
            std::time::Duration::from_secs(1),
        ));
        assert!(minimal_pending_hint(&silent).is_none());
        let expired = Some(PendingAction::with_ttl(
            Action::Quit,
            shortcut,
            Some("quit"),
            std::time::Duration::ZERO,
        ));
        assert!(minimal_pending_hint(&expired).is_none());
    }
}
