use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect;
use crate::app::App;
use crate::runtime::EventResult;
use crate::ui::page;
use crate::ui::state::app_mode::AppMode;
pub(crate) fn handle(app: &mut App, content_area: Rect, key: KeyEvent) -> EventResult {
if matches!(key.code, KeyCode::Esc | KeyCode::Char('q')) {
app.mode = AppMode::List;
return EventResult::Continue;
}
let max_scroll_offset = match &app.mode {
AppMode::ReviewDetail { review, .. } => {
page::review_detail::review_detail_max_scroll_offset(
review,
content_area,
app.markdown_render_cache(),
)
}
_ => 0,
};
if let AppMode::ReviewDetail { scroll_offset, .. } = &mut app.mode {
let clamped_scroll_offset = (*scroll_offset).min(max_scroll_offset);
*scroll_offset = match key.code {
KeyCode::Down | KeyCode::Char('j') if key.modifiers == KeyModifiers::NONE => {
clamped_scroll_offset
.saturating_add(1)
.min(max_scroll_offset)
}
KeyCode::Up | KeyCode::Char('k') if key.modifiers == KeyModifiers::NONE => {
clamped_scroll_offset.saturating_sub(1)
}
KeyCode::Char('d') if key.modifiers == KeyModifiers::CONTROL => clamped_scroll_offset
.saturating_add(review_detail_half_page_step(content_area))
.min(max_scroll_offset),
KeyCode::Char('u') if key.modifiers == KeyModifiers::CONTROL => {
clamped_scroll_offset.saturating_sub(review_detail_half_page_step(content_area))
}
KeyCode::Char('g') if key.modifiers == KeyModifiers::NONE => 0,
KeyCode::Char('G') if key.modifiers == KeyModifiers::SHIFT => max_scroll_offset,
KeyCode::Char('G') if key.modifiers == KeyModifiers::NONE => max_scroll_offset,
_ => clamped_scroll_offset,
};
}
EventResult::Continue
}
fn review_detail_half_page_step(content_area: Rect) -> u16 {
content_area
.height
.saturating_sub(5)
.saturating_div(2)
.max(1)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use ag_forge::{ForgeKind, RequestedReview, RequestedReviewAudience};
use crossterm::event::KeyModifiers;
use super::*;
use crate::db::Database;
#[tokio::test]
async fn test_handle_q_returns_to_list_mode() {
let mut app = new_test_app().await;
let event_result = handle(
&mut app,
Rect::new(0, 0, 80, 20),
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(app.mode, AppMode::List));
}
#[tokio::test]
async fn test_handle_scrolls_detail_page_down_and_up() {
let mut app = new_test_app().await;
app.mode = AppMode::ReviewDetail {
review: requested_review("line 1\nline 2\nline 3\nline 4\nline 5\nline 6"),
scroll_offset: 0,
};
handle(
&mut app,
Rect::new(0, 0, 80, 8),
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
);
handle(
&mut app,
Rect::new(0, 0, 80, 8),
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::ReviewDetail {
scroll_offset: 0,
..
}
));
}
#[tokio::test]
async fn test_handle_scrolls_detail_page_to_bottom() {
let mut app = new_test_app().await;
app.mode = AppMode::ReviewDetail {
review: requested_review("line 1\nline 2\nline 3\nline 4\nline 5\nline 6"),
scroll_offset: 0,
};
handle(
&mut app,
Rect::new(0, 0, 80, 8),
KeyEvent::new(KeyCode::Char('G'), KeyModifiers::SHIFT),
);
assert!(matches!(
app.mode,
AppMode::ReviewDetail {
scroll_offset: 6,
..
}
));
}
async fn new_test_app() -> App {
let base_dir = tempfile::tempdir().expect("failed to create temp dir");
let database = Database::open_in_memory()
.await
.expect("failed to open in-memory db");
let mut app = App::new_with_clients(
base_dir.path().to_path_buf(),
base_dir.path().to_path_buf(),
None,
database,
test_app_clients(),
)
.await
.expect("failed to build app");
app.mode = AppMode::ReviewDetail {
review: requested_review("Detail body"),
scroll_offset: 0,
};
app
}
fn test_app_clients() -> crate::app::AppClients {
crate::app::AppClients::new().with_agent_availability_probe(Arc::new(
crate::infra::agent::StaticAgentAvailabilityProbe {
available_agent_kinds: crate::domain::agent::AgentKind::ALL.to_vec(),
},
))
}
fn requested_review(body: &str) -> RequestedReview {
RequestedReview {
audience: RequestedReviewAudience::Personal,
body: Some(body.to_string()),
display_id: "#42".to_string(),
forge_kind: ForgeKind::GitHub,
repository: "agentty-xyz/agentty".to_string(),
status_summary: None,
title: "Add review detail page".to_string(),
updated_at: Some("2026-04-27T21:30:00Z".to_string()),
web_url: "https://example.com/42".to_string(),
}
}
}