use super::*;
pub(crate) fn rail_panel_shortcut(app: &mut App, panel: crate::tui::work_surface::RailPanel) {
app.work_surface.panel = panel;
app.needs_redraw = true;
let mut message = format!("Rail panel: {}", panel.as_setting());
if app.work_surface.placement == crate::tui::work_surface::WorkSurfacePlacement::Off {
message.push_str(" (rail is off — /rail top to show)");
}
app.status_message = Some(message);
}
pub(crate) fn agent_progress_redraw_permitted(
last_redraw: &mut Option<Instant>,
now: Instant,
) -> bool {
match *last_redraw {
Some(last) if now.duration_since(last) < Duration::from_millis(100) => false,
_ => {
*last_redraw = Some(now);
true
}
}
}
pub(crate) fn workflow_budget_redraw_permitted(
last_redraw: &mut Option<Instant>,
now: Instant,
) -> bool {
agent_progress_redraw_permitted(last_redraw, now)
}
pub(crate) fn agent_progress_redraw_permitted_for_drain(
last_redraw: &mut Option<Instant>,
seen_agents: &mut HashSet<String>,
agent_id: &str,
now: Instant,
) -> bool {
if !seen_agents.insert(agent_id.to_string()) {
return false;
}
agent_progress_redraw_permitted(last_redraw, now)
}
pub(crate) fn rail_row_budget(
app: &App,
terminal_width: u16,
terminal_height: u16,
idle_empty: bool,
) -> u16 {
let ambient_mark_can_draw =
idle_empty && terminal_width >= crate::tui::underwater::AMBIENT_MIN_CHAT_WIDTH;
let chat_floor = if ambient_mark_can_draw {
crate::tui::underwater::AMBIENT_MIN_CHAT_HEIGHT
} else {
MIN_CHAT_HEIGHT
};
let composer_floor = MIN_COMPOSER_HEIGHT.saturating_add(u16::from(app.composer_border));
terminal_height
.saturating_sub(header_height_for(terminal_height))
.saturating_sub(crate::tui::phase_strip::height())
.saturating_sub(composer_floor)
.saturating_sub(chat_floor)
}
pub(crate) fn header_height_for(terminal_height: u16) -> u16 {
if terminal_height < 16 { 1 } else { 2 }
}
pub(crate) fn rail_min_chat_width(idle_empty: bool) -> u16 {
if idle_empty {
crate::tui::underwater::AMBIENT_MIN_CHAT_WIDTH
} else {
0
}
}
pub(crate) fn status_color(level: StatusToastLevel) -> ratatui::style::Color {
match level {
StatusToastLevel::Info => palette::WHALE_INFO,
StatusToastLevel::Success => palette::STATUS_SUCCESS,
StatusToastLevel::Warning => palette::STATUS_WARNING,
StatusToastLevel::Error => palette::STATUS_ERROR,
}
}
pub(crate) fn status_animation_interval_ms(app: &App) -> u64 {
if app.effective_low_motion_for_status() {
crate::tui::display_refresh::adaptive_animation_interval_ms(true)
} else {
UI_STATUS_ANIMATION_MS
}
}
pub(crate) fn underwater_animation_interval_ms(app: &App) -> u64 {
if app.effective_low_motion_for_status() || app.low_motion {
crate::tui::display_refresh::adaptive_animation_interval_ms(true)
} else {
crate::tui::display_refresh::adaptive_animation_interval_ms(false)
.min(UI_UNDERWATER_ANIMATION_MS)
}
}
#[must_use]
pub(crate) fn underwater_motion_surface_visible(
area: Option<Rect>,
ombre_field_breathes: bool,
empty_water_visible: bool,
obscured: bool,
) -> bool {
if obscured {
return false;
}
area.is_some_and(|area| {
area.width > 0
&& area.height > 0
&& (ombre_field_breathes
|| (area.width >= crate::tui::ocean::AMBIENT_MIN_WIDTH
&& area.height >= crate::tui::ocean::AMBIENT_MIN_HEIGHT)
|| (empty_water_visible && crate::tui::underwater::empty_state_mark_visible(area)))
})
}
pub(crate) fn animation_interval_ms(
app: &App,
status_motion: bool,
underwater_motion: bool,
) -> u64 {
let underwater = underwater_animation_interval_ms(app);
match (status_motion, underwater_motion) {
(true, true) => status_animation_interval_ms(app).min(underwater),
(true, false) => status_animation_interval_ms(app),
(false, true) => underwater,
(false, false) => underwater,
}
}
pub(crate) fn should_tick_status_animation(
app: &App,
has_running_agents: bool,
history_has_live_motion: bool,
active_cell_has_live_motion: bool,
translation_placeholder_has_live_motion: bool,
) -> bool {
!matches!(app.motion_policy().mode(), MotionMode::Still)
&& (app.is_loading
|| has_running_agents
|| app.is_compacting
|| app.is_purging
|| history_has_live_motion
|| active_cell_has_live_motion
|| translation_placeholder_has_live_motion
|| visible_background_task_has_live_motion(app))
}
pub(crate) fn visible_background_task_has_live_motion(app: &App) -> bool {
app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks
&& app.work_surface.last_area.is_some()
&& app.task_panel.iter().any(|task| task.status == "running")
}
pub(crate) fn active_cell_has_live_motion(app: &App) -> bool {
app.active_cell
.as_ref()
.is_some_and(|active| active.entries().iter().any(HistoryCell::has_live_motion))
}
pub(crate) fn history_has_live_motion(history: &[HistoryCell]) -> bool {
history.iter().any(HistoryCell::has_live_motion)
}