use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Color, Line, Modifier, Span, Style, Text};
use ratatui::widgets::{Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table, Tabs, Wrap};
use crate::tui::model::{
CODEX_RECENT_WINDOWS, Palette, codex_recent_window_label, codex_recent_window_threshold_ms,
format_age, now_ms, shorten_middle,
};
use crate::tui::state::UiState;
pub(super) fn render_recent_page(f: &mut Frame<'_>, p: Palette, ui: &mut UiState, area: Rect) {
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(65), Constraint::Percentage(35)])
.split(area);
let now = now_ms();
let threshold_ms = codex_recent_window_threshold_ms(now, ui.codex_recent_window_idx);
let visible = ui
.codex_recent_rows
.iter()
.filter(|r| r.mtime_ms >= threshold_ms)
.take(300)
.collect::<Vec<_>>();
let selected_idx_in_visible = ui
.codex_recent_selected_id
.as_deref()
.and_then(|sid| visible.iter().position(|r| r.session_id.as_str() == sid))
.unwrap_or(ui.codex_recent_selected_idx)
.min(visible.len().saturating_sub(1));
ui.codex_recent_selected_idx = selected_idx_in_visible;
ui.codex_recent_selected_id = visible
.get(ui.codex_recent_selected_idx)
.map(|r| r.session_id.clone());
if visible.is_empty() {
ui.codex_recent_table.select(None);
} else {
ui.codex_recent_table
.select(Some(ui.codex_recent_selected_idx));
}
let title = format!(
"{} (window: {}, raw_cwd: {})",
crate::tui::i18n::pick(ui.language, "最近会话 (Codex)", "Recent sessions (Codex)"),
codex_recent_window_label(ui.codex_recent_window_idx),
if ui.codex_recent_raw_cwd { "on" } else { "off" }
);
let left_block = Block::default()
.title(Span::styled(
title,
Style::default().fg(p.text).add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(p.border))
.style(Style::default().bg(p.panel));
let left_inner = left_block.inner(columns[0]);
f.render_widget(left_block, columns[0]);
let inner_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Min(1)])
.split(left_inner);
let tabs = Tabs::new(
CODEX_RECENT_WINDOWS
.iter()
.map(|(_, label)| Line::from(Span::raw(*label)))
.collect::<Vec<_>>(),
)
.select(
ui.codex_recent_window_idx
.min(CODEX_RECENT_WINDOWS.len().saturating_sub(1)),
)
.style(Style::default().fg(p.muted))
.highlight_style(Style::default().fg(p.text).add_modifier(Modifier::BOLD))
.divider(Span::raw(" "));
f.render_widget(tabs, inner_chunks[0]);
let rows = visible
.iter()
.map(|r| {
let root = shorten_middle(r.root.as_str(), 120);
let branch = r.branch.as_deref().unwrap_or("-");
let age = format_age(now, Some(r.mtime_ms));
let line1 = Line::from(vec![
Span::styled(root, Style::default().fg(p.text)),
Span::raw(" "),
Span::styled(
format!("[{branch}]"),
Style::default().fg(if r.branch.is_some() {
p.accent
} else {
p.muted
}),
),
]);
let line2 = Line::from(vec![
Span::styled(r.session_id.clone(), Style::default().fg(p.text)),
Span::raw(" "),
Span::styled(age, Style::default().fg(p.muted)),
]);
let text = Text::from(vec![line1, line2]);
Row::new(vec![Cell::from(text)]).height(2)
})
.collect::<Vec<_>>();
let table = Table::new(rows, [Constraint::Min(10)])
.row_highlight_style(Style::default().bg(Color::Rgb(32, 39, 48)))
.highlight_symbol(" ")
.highlight_spacing(HighlightSpacing::Always);
f.render_stateful_widget(table, inner_chunks[1], &mut ui.codex_recent_table);
let right_block = Block::default()
.title(Span::styled(
crate::tui::i18n::pick(ui.language, "详情", "Details"),
Style::default().fg(p.text).add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(p.border))
.style(Style::default().bg(p.panel));
let mut lines = Vec::new();
if let Some(err) = ui.codex_recent_error.as_deref() {
lines.push(Line::from(Span::styled(
format!("error: {err}"),
Style::default().fg(p.bad),
)));
lines.push(Line::from(""));
}
if ui.codex_recent_rows.is_empty() {
lines.push(Line::from(Span::styled(
crate::tui::i18n::pick(
ui.language,
"未加载最近会话。按 r 刷新;或确认 ~/.codex/sessions 存在。",
"No recent sessions loaded. Press r to refresh; or check ~/.codex/sessions.",
),
Style::default().fg(p.muted),
)));
} else if let Some(r) = visible.get(ui.codex_recent_selected_idx) {
let branch = r.branch.as_deref().unwrap_or("-");
lines.push(Line::from(vec![
Span::styled("root: ", Style::default().fg(p.muted)),
Span::styled(r.root.clone(), Style::default().fg(p.text)),
]));
lines.push(Line::from(vec![
Span::styled("branch: ", Style::default().fg(p.muted)),
Span::styled(branch.to_string(), Style::default().fg(p.text)),
]));
lines.push(Line::from(vec![
Span::styled("sid: ", Style::default().fg(p.muted)),
Span::styled(r.session_id.clone(), Style::default().fg(p.text)),
]));
lines.push(Line::from(vec![
Span::styled("mtime: ", Style::default().fg(p.muted)),
Span::styled(
format_age(now, Some(r.mtime_ms)),
Style::default().fg(p.muted),
),
]));
lines.push(Line::from(vec![
Span::styled("cwd: ", Style::default().fg(p.muted)),
Span::styled(
r.cwd
.as_deref()
.map(|v| shorten_middle(v, 120))
.unwrap_or_else(|| "-".to_string()),
Style::default().fg(p.text),
),
]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("copy: ", Style::default().fg(p.muted)),
Span::styled(
format!("{} {}", r.root, r.session_id),
Style::default().fg(p.accent),
),
]));
} else {
lines.push(Line::from(Span::styled(
crate::tui::i18n::pick(ui.language, "未选中任何条目。", "No selection."),
Style::default().fg(p.muted),
)));
}
let content = Paragraph::new(Text::from(lines))
.block(right_block)
.style(Style::default().fg(p.text))
.wrap(Wrap { trim: false });
f.render_widget(content, columns[1]);
}