use std::collections::HashMap;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::TableState;
use crate::app::{SettingsManager, Tab};
use crate::domain::agent::ReasoningLevel;
use crate::domain::input::InputState;
use crate::domain::project::ProjectListItem;
use crate::domain::session::{DailyActivity, Session, SessionId};
use crate::infra::review_comment_cache::ReviewCommentCache;
use crate::ui::overlay::{
HelpOverlayRenderContext, SyncBlockedPopupRenderContext, ViewInfoPopupRenderContext,
};
use crate::ui::state::app_mode::{
AppMode, ConfirmationIntent, ConfirmationViewMode, DiffRightPanel,
};
use crate::ui::{Component, Page, RenderContext, component, markdown, overlay, page};
pub(crate) struct ListBackgroundRenderContext<'a> {
pub(crate) active_project_id: i64,
pub(crate) current_tab: Tab,
pub(crate) has_tasks_tab: bool,
pub(crate) project_table_state: &'a mut TableState,
pub(crate) projects: &'a [ProjectListItem],
pub(crate) sessions: &'a [Session],
pub(crate) settings: &'a mut SettingsManager,
pub(crate) stats_activity: &'a [DailyActivity],
pub(crate) task_roadmap: Option<&'a str>,
pub(crate) task_roadmap_error: Option<&'a str>,
pub(crate) task_roadmap_scroll_offset: u16,
pub(crate) table_state: &'a mut TableState,
}
struct RouteSharedContext<'a> {
active_project_id: i64,
current_tab: Tab,
has_tasks_tab: bool,
project_table_state: &'a mut TableState,
projects: &'a [ProjectListItem],
sessions: &'a [Session],
settings: &'a mut SettingsManager,
stats_activity: &'a [DailyActivity],
task_roadmap: Option<&'a str>,
task_roadmap_error: Option<&'a str>,
task_roadmap_scroll_offset: u16,
table_state: &'a mut TableState,
}
impl RouteSharedContext<'_> {
fn list_background(&mut self) -> ListBackgroundRenderContext<'_> {
ListBackgroundRenderContext {
active_project_id: self.active_project_id,
current_tab: self.current_tab,
has_tasks_tab: self.has_tasks_tab,
project_table_state: self.project_table_state,
projects: self.projects,
sessions: self.sessions,
settings: self.settings,
stats_activity: self.stats_activity,
task_roadmap: self.task_roadmap,
task_roadmap_error: self.task_roadmap_error,
task_roadmap_scroll_offset: self.task_roadmap_scroll_offset,
table_state: self.table_state,
}
}
}
#[derive(Clone, Copy)]
struct SessionChatRenderContext<'a> {
active_prompt_outputs: &'a HashMap<SessionId, String>,
default_reasoning_level: ReasoningLevel,
markdown_render_cache: &'a markdown::MarkdownRenderCache,
mode: &'a AppMode,
output_layout_cache: &'a component::session_output::SessionOutputLayoutCache,
session_id: &'a str,
session_progress_messages: &'a HashMap<SessionId, String>,
session_update_versions: &'a HashMap<SessionId, u64>,
session_worktree_availability: &'a HashMap<SessionId, bool>,
sessions: &'a [Session],
scroll_offset: Option<u16>,
wall_clock_unix_seconds: i64,
}
#[derive(Clone, Copy)]
struct PublishBranchOverlayContext<'a> {
default_branch_name: &'a str,
active_prompt_outputs: &'a HashMap<SessionId, String>,
default_reasoning_level: ReasoningLevel,
markdown_render_cache: &'a markdown::MarkdownRenderCache,
output_layout_cache: &'a component::session_output::SessionOutputLayoutCache,
input: &'a InputState,
locked_upstream_ref: Option<&'a str>,
restore_view: &'a ConfirmationViewMode,
session_progress_messages: &'a HashMap<SessionId, String>,
session_update_versions: &'a HashMap<SessionId, u64>,
session_worktree_availability: &'a HashMap<SessionId, bool>,
sessions: &'a [Session],
}
#[derive(Clone, Copy)]
struct RouteAuxContext<'a> {
active_prompt_outputs: &'a HashMap<SessionId, String>,
default_reasoning_level: ReasoningLevel,
markdown_render_cache: &'a markdown::MarkdownRenderCache,
output_layout_cache: &'a component::session_output::SessionOutputLayoutCache,
review_comment_cache: &'a ReviewCommentCache,
session_progress_messages: &'a HashMap<SessionId, String>,
session_update_versions: &'a HashMap<SessionId, u64>,
session_worktree_availability: &'a HashMap<SessionId, bool>,
wall_clock_unix_seconds: i64,
}
pub(crate) fn route_frame(f: &mut Frame, area: Rect, context: RenderContext<'_>) {
let RenderContext {
active_project_id,
active_prompt_outputs,
current_tab,
has_tasks_tab,
markdown_render_cache,
mode,
output_layout_cache,
project_table_state,
projects,
review_comment_cache,
session_progress_messages,
session_update_versions,
session_worktree_availability,
settings,
stats_activity,
task_roadmap,
task_roadmap_error,
task_roadmap_scroll_offset,
sessions,
table_state,
wall_clock_unix_seconds,
..
} = context;
let mut shared = RouteSharedContext {
active_project_id,
current_tab,
has_tasks_tab,
project_table_state,
projects,
sessions,
settings,
stats_activity,
task_roadmap,
task_roadmap_error,
task_roadmap_scroll_offset,
table_state,
};
let aux = RouteAuxContext {
active_prompt_outputs,
default_reasoning_level: shared.settings.reasoning_level,
markdown_render_cache,
output_layout_cache,
review_comment_cache,
session_progress_messages,
session_update_versions,
session_worktree_availability,
wall_clock_unix_seconds,
};
if render_list_or_overlay_mode(f, area, mode, &mut shared, aux) {
return;
}
render_session_or_diff_mode(f, area, mode, shared.sessions, aux);
}
fn render_list_or_overlay_mode(
f: &mut Frame,
area: Rect,
mode: &AppMode,
shared: &mut RouteSharedContext<'_>,
aux: RouteAuxContext<'_>,
) -> bool {
match mode {
AppMode::List => render_list_background(
f,
area,
shared.list_background(),
aux.wall_clock_unix_seconds,
),
AppMode::SessionCreation {
selected_option_index,
} => overlay::render_session_creation_overlay(
f,
area,
shared.list_background(),
*selected_option_index,
aux.wall_clock_unix_seconds,
),
AppMode::Confirmation { .. } => render_confirmation_mode(f, area, mode, shared, aux),
AppMode::SyncBlockedPopup {
default_branch,
is_loading,
message,
project_name,
title,
} => overlay::render_sync_blocked_popup(
f,
area,
shared.list_background(),
aux.wall_clock_unix_seconds,
SyncBlockedPopupRenderContext {
default_branch: default_branch.as_deref(),
is_loading: *is_loading,
message,
project_name: project_name.as_deref(),
title,
},
),
AppMode::ViewInfoPopup { .. } => {
render_view_info_popup_mode(f, area, mode, shared.sessions, aux);
}
AppMode::Help {
context: help_context,
scroll_offset,
} => overlay::render_help(
f,
area,
HelpOverlayRenderContext {
help_context,
list_background: shared.list_background(),
markdown_render_cache: aux.markdown_render_cache,
output_layout_cache: aux.output_layout_cache,
review_comment_cache: aux.review_comment_cache,
scroll_offset: *scroll_offset,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
wall_clock_unix_seconds: aux.wall_clock_unix_seconds,
},
),
AppMode::View { .. }
| AppMode::Prompt { .. }
| AppMode::Question { .. }
| AppMode::PublishBranchInput { .. }
| AppMode::OpenCommandSelector { .. }
| AppMode::Diff { .. } => {
return false;
}
}
true
}
fn render_confirmation_mode(
f: &mut Frame,
area: Rect,
mode: &AppMode,
shared: &mut RouteSharedContext<'_>,
aux: RouteAuxContext<'_>,
) {
let AppMode::Confirmation {
confirmation_intent,
confirmation_message,
confirmation_title,
restore_view,
selected_confirmation_index,
..
} = mode
else {
return;
};
if matches!(
confirmation_intent,
ConfirmationIntent::ContinueSession
| ConfirmationIntent::MergeSession
| ConfirmationIntent::RegenerateReview
) && let Some(view_mode) = restore_view
{
render_session_confirmation_overlay(
f,
area,
SessionOverlayRenderContext {
active_prompt_outputs: aux.active_prompt_outputs,
default_reasoning_level: aux.default_reasoning_level,
markdown_render_cache: aux.markdown_render_cache,
output_layout_cache: aux.output_layout_cache,
restore_view: view_mode,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
session_worktree_availability: aux.session_worktree_availability,
sessions: shared.sessions,
wall_clock_unix_seconds: aux.wall_clock_unix_seconds,
},
&SessionConfirmationContext {
confirmation_message,
confirmation_title,
selected_confirmation_index: *selected_confirmation_index,
},
);
return;
}
overlay::render_confirmation_overlay(
f,
area,
mode,
shared.list_background(),
aux.wall_clock_unix_seconds,
);
}
fn render_view_info_popup_mode(
f: &mut Frame,
area: Rect,
mode: &AppMode,
sessions: &[Session],
aux: RouteAuxContext<'_>,
) {
let AppMode::ViewInfoPopup {
is_loading,
loading_label,
message,
restore_view,
title,
} = mode
else {
return;
};
overlay::render_view_info_popup(
f,
area,
ViewInfoPopupRenderContext {
can_open_worktree: *aux
.session_worktree_availability
.get(&restore_view.session_id)
.unwrap_or(&false),
default_reasoning_level: aux.default_reasoning_level,
markdown_render_cache: aux.markdown_render_cache,
output_layout_cache: aux.output_layout_cache,
is_loading: *is_loading,
loading_label,
message,
restore_view,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
sessions,
title,
wall_clock_unix_seconds: aux.wall_clock_unix_seconds,
},
);
}
struct SessionConfirmationContext<'a> {
confirmation_message: &'a str,
confirmation_title: &'a str,
selected_confirmation_index: usize,
}
#[derive(Clone, Copy)]
struct SessionOverlayRenderContext<'a> {
active_prompt_outputs: &'a HashMap<SessionId, String>,
default_reasoning_level: ReasoningLevel,
markdown_render_cache: &'a markdown::MarkdownRenderCache,
output_layout_cache: &'a component::session_output::SessionOutputLayoutCache,
restore_view: &'a ConfirmationViewMode,
session_progress_messages: &'a HashMap<SessionId, String>,
session_update_versions: &'a HashMap<SessionId, u64>,
session_worktree_availability: &'a HashMap<SessionId, bool>,
sessions: &'a [Session],
wall_clock_unix_seconds: i64,
}
fn render_session_overlay_background(
f: &mut Frame,
area: Rect,
context: SessionOverlayRenderContext<'_>,
) {
let background_mode = context.restore_view.clone().into_view_mode();
render_session_chat(
f,
area,
SessionChatRenderContext {
active_prompt_outputs: context.active_prompt_outputs,
default_reasoning_level: context.default_reasoning_level,
markdown_render_cache: context.markdown_render_cache,
mode: &background_mode,
output_layout_cache: context.output_layout_cache,
session_id: &context.restore_view.session_id,
session_progress_messages: context.session_progress_messages,
session_update_versions: context.session_update_versions,
session_worktree_availability: context.session_worktree_availability,
sessions: context.sessions,
scroll_offset: context.restore_view.scroll_offset,
wall_clock_unix_seconds: context.wall_clock_unix_seconds,
},
);
overlay::render_overlay_backdrop(f, area);
}
fn render_session_confirmation_overlay(
f: &mut Frame,
area: Rect,
overlay_context: SessionOverlayRenderContext<'_>,
confirmation_context: &SessionConfirmationContext<'_>,
) {
render_session_overlay_background(f, area, overlay_context);
component::confirmation_overlay::ConfirmationOverlay::new(
confirmation_context.confirmation_title,
confirmation_context.confirmation_message,
)
.selected_yes(confirmation_context.selected_confirmation_index == 0)
.render(f, area);
}
fn render_session_or_diff_mode(
f: &mut Frame,
area: Rect,
mode: &AppMode,
sessions: &[Session],
aux: RouteAuxContext<'_>,
) {
match mode {
AppMode::View {
session_id,
scroll_offset,
..
}
| AppMode::Prompt {
session_id,
scroll_offset,
..
}
| AppMode::Question {
session_id,
scroll_offset,
..
} => render_session_chat(
f,
area,
SessionChatRenderContext {
active_prompt_outputs: aux.active_prompt_outputs,
default_reasoning_level: aux.default_reasoning_level,
markdown_render_cache: aux.markdown_render_cache,
mode,
output_layout_cache: aux.output_layout_cache,
session_id,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
session_worktree_availability: aux.session_worktree_availability,
sessions,
scroll_offset: *scroll_offset,
wall_clock_unix_seconds: aux.wall_clock_unix_seconds,
},
),
AppMode::OpenCommandSelector {
commands,
restore_view,
selected_command_index,
} => render_open_command_selector_overlay(
f,
area,
SessionOverlayRenderContext {
active_prompt_outputs: aux.active_prompt_outputs,
default_reasoning_level: aux.default_reasoning_level,
markdown_render_cache: aux.markdown_render_cache,
output_layout_cache: aux.output_layout_cache,
restore_view,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
session_worktree_availability: aux.session_worktree_availability,
sessions,
wall_clock_unix_seconds: aux.wall_clock_unix_seconds,
},
commands,
*selected_command_index,
),
AppMode::PublishBranchInput {
default_branch_name,
input,
locked_upstream_ref,
restore_view,
..
} => render_publish_branch_input_mode(
f,
area,
PublishBranchModeContext {
default_branch_name,
input,
locked_upstream_ref: locked_upstream_ref.as_deref(),
restore_view,
},
sessions,
aux,
),
AppMode::Diff {
diff,
file_explorer_selected_index,
restore_question: _,
right_panel,
scroll_cache: _,
scroll_offset,
session_id,
} => render_diff_mode(
f,
area,
sessions,
session_id,
diff,
*scroll_offset,
*file_explorer_selected_index,
*right_panel,
aux.markdown_render_cache,
aux.review_comment_cache,
),
AppMode::List
| AppMode::SessionCreation { .. }
| AppMode::Confirmation { .. }
| AppMode::SyncBlockedPopup { .. }
| AppMode::ViewInfoPopup { .. }
| AppMode::Help { .. } => {}
}
}
fn render_publish_branch_input_mode(
f: &mut Frame,
area: Rect,
mode_context: PublishBranchModeContext<'_>,
sessions: &[Session],
aux: RouteAuxContext<'_>,
) {
render_publish_branch_overlay(
f,
area,
&PublishBranchOverlayContext {
default_branch_name: mode_context.default_branch_name,
active_prompt_outputs: aux.active_prompt_outputs,
default_reasoning_level: aux.default_reasoning_level,
markdown_render_cache: aux.markdown_render_cache,
output_layout_cache: aux.output_layout_cache,
input: mode_context.input,
locked_upstream_ref: mode_context.locked_upstream_ref,
restore_view: mode_context.restore_view,
session_progress_messages: aux.session_progress_messages,
session_update_versions: aux.session_update_versions,
session_worktree_availability: aux.session_worktree_availability,
sessions,
},
aux.wall_clock_unix_seconds,
);
}
#[derive(Clone, Copy)]
struct PublishBranchModeContext<'a> {
default_branch_name: &'a str,
input: &'a InputState,
locked_upstream_ref: Option<&'a str>,
restore_view: &'a ConfirmationViewMode,
}
fn render_open_command_selector_overlay(
f: &mut Frame,
area: Rect,
overlay_context: SessionOverlayRenderContext<'_>,
commands: &[String],
selected_command_index: usize,
) {
render_session_overlay_background(f, area, overlay_context);
component::open_command_overlay::OpenCommandOverlay::new(commands)
.selected_command_index(selected_command_index)
.render(f, area);
}
fn render_publish_branch_overlay(
f: &mut Frame,
area: Rect,
context: &PublishBranchOverlayContext<'_>,
wall_clock_unix_seconds: i64,
) {
let PublishBranchOverlayContext {
default_branch_name,
active_prompt_outputs,
default_reasoning_level,
markdown_render_cache,
output_layout_cache,
input,
locked_upstream_ref,
restore_view,
session_progress_messages,
session_update_versions,
session_worktree_availability,
sessions,
} = *context;
render_session_overlay_background(
f,
area,
SessionOverlayRenderContext {
active_prompt_outputs,
default_reasoning_level,
markdown_render_cache,
output_layout_cache,
restore_view,
session_progress_messages,
session_update_versions,
session_worktree_availability,
sessions,
wall_clock_unix_seconds,
},
);
component::publish_branch_overlay::PublishBranchOverlay::new(
input,
default_branch_name,
locked_upstream_ref,
)
.render(f, area);
}
fn render_session_chat(f: &mut Frame, area: Rect, context: SessionChatRenderContext<'_>) {
let SessionChatRenderContext {
active_prompt_outputs,
default_reasoning_level,
markdown_render_cache,
mode,
output_layout_cache,
session_id,
session_progress_messages,
session_update_versions,
session_worktree_availability,
sessions,
scroll_offset,
wall_clock_unix_seconds,
} = context;
let Some(session_index) = sessions.iter().position(|session| session.id == session_id) else {
return;
};
let active_progress = session_progress_messages
.get(session_id)
.map(std::string::String::as_str);
let active_prompt_output = active_prompt_outputs
.get(session_id)
.map(std::string::String::as_str);
let session_update_version = session_update_versions
.get(session_id)
.copied()
.unwrap_or_default();
page::session_chat::SessionChatPage::new(page::session_chat::SessionChatPageInput {
active_prompt_output,
active_progress,
default_reasoning_level,
markdown_render_cache,
mode,
output_layout_cache,
scroll_offset,
session_index,
session_update_version,
sessions,
wall_clock_unix_seconds,
})
.can_open_worktree(
*session_worktree_availability
.get(session_id)
.unwrap_or(&false),
)
.render(f, area);
}
#[allow(clippy::too_many_arguments)]
fn render_diff_mode(
f: &mut Frame,
area: Rect,
sessions: &[Session],
session_id: &SessionId,
diff: &str,
scroll_offset: u16,
file_explorer_selected_index: usize,
right_panel: DiffRightPanel,
markdown_render_cache: &markdown::MarkdownRenderCache,
review_comment_cache: &ReviewCommentCache,
) {
if let Some(session) = sessions.iter().find(|session| &session.id == session_id) {
let snapshot = review_comment_cache.snapshot(session_id);
page::diff::DiffPage::new(
session,
diff.to_string(),
scroll_offset,
file_explorer_selected_index,
right_panel,
markdown_render_cache,
snapshot.as_ref(),
)
.render(f, area);
}
}
pub(crate) fn render_list_background(
f: &mut Frame,
content_area: Rect,
context: ListBackgroundRenderContext<'_>,
wall_clock_unix_seconds: i64,
) {
let ListBackgroundRenderContext {
active_project_id,
current_tab,
has_tasks_tab,
project_table_state,
projects,
sessions,
settings,
stats_activity,
task_roadmap,
task_roadmap_error,
task_roadmap_scroll_offset,
table_state,
} = context;
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(content_area);
component::tab::Tabs::new(current_tab, active_project_id, has_tasks_tab, projects)
.render(f, chunks[0]);
match current_tab {
Tab::Projects => {
page::project_list::ProjectListPage::new(
projects,
project_table_state,
active_project_id,
)
.render(f, chunks[1]);
}
Tab::Sessions => {
page::session_list::SessionListPage::new(
sessions,
table_state,
wall_clock_unix_seconds,
)
.render(f, chunks[1]);
}
Tab::Tasks => {
let mut page = page::task::TasksPage::new(
task_roadmap,
task_roadmap_error,
task_roadmap_scroll_offset,
);
page.render(f, chunks[1]);
}
Tab::Stats => {
page::stat::StatsPage::new(sessions, stats_activity).render(f, chunks[1]);
}
Tab::Settings => {
let active_project_name = active_project_name(active_project_id, projects);
page::setting::SettingsPage::new(settings, active_project_name).render(f, chunks[1]);
}
}
}
fn active_project_name(active_project_id: i64, projects: &[ProjectListItem]) -> Option<String> {
projects
.iter()
.find(|project_item| project_item.project.id == active_project_id)
.map(|project_item| project_item.project.display_label())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use ratatui::widgets::Paragraph;
use super::*;
use crate::domain::agent::ReasoningLevel;
use crate::domain::session::Status;
use crate::domain::session::tests::SessionFixtureBuilder;
fn session_fixture(session_id: &str) -> Session {
SessionFixtureBuilder::new()
.id(session_id)
.folder(PathBuf::from(format!("/tmp/{session_id}")))
.output("Captured output")
.prompt("Prompt")
.status(Status::Review)
.summary(Some("Summary line for router test".to_string()))
.title(Some("Router Session".to_string()))
.build()
}
fn buffer_text(buffer: &ratatui::buffer::Buffer) -> String {
buffer
.content()
.iter()
.map(ratatui::buffer::Cell::symbol)
.collect()
}
#[test]
fn render_session_or_diff_mode_renders_view_session_content() {
let backend = ratatui::backend::TestBackend::new(120, 30);
let mut terminal = ratatui::Terminal::new(backend).expect("failed to create terminal");
let session_id = "session-1234";
let sessions = vec![session_fixture(session_id)];
let mode = AppMode::View {
review_status_message: None,
review_text: None,
session_id: session_id.into(),
scroll_offset: None,
};
let progress_messages = HashMap::new();
let cache = markdown::MarkdownRenderCache::default();
let review_comment_cache = ReviewCommentCache::default();
let output_layout_cache = component::session_output::SessionOutputLayoutCache::default();
let session_update_versions = HashMap::new();
terminal
.draw(|frame| {
render_session_or_diff_mode(
frame,
frame.area(),
&mode,
&sessions,
RouteAuxContext {
active_prompt_outputs: &HashMap::new(),
default_reasoning_level: ReasoningLevel::default(),
markdown_render_cache: &cache,
output_layout_cache: &output_layout_cache,
review_comment_cache: &review_comment_cache,
session_progress_messages: &progress_messages,
session_update_versions: &session_update_versions,
session_worktree_availability: &HashMap::new(),
wall_clock_unix_seconds: 0,
},
);
})
.expect("failed to draw");
let text = buffer_text(terminal.backend().buffer());
assert!(text.contains("Router Session"));
assert!(text.contains("Captured output"));
}
#[test]
fn render_session_or_diff_mode_keeps_background_when_session_is_missing() {
let backend = ratatui::backend::TestBackend::new(80, 20);
let mut terminal = ratatui::Terminal::new(backend).expect("failed to create terminal");
let mode = AppMode::View {
review_status_message: None,
review_text: None,
session_id: "missing-session".into(),
scroll_offset: None,
};
let progress_messages = HashMap::new();
let sessions = Vec::new();
let cache = markdown::MarkdownRenderCache::default();
let review_comment_cache = ReviewCommentCache::default();
let output_layout_cache = component::session_output::SessionOutputLayoutCache::default();
let session_update_versions = HashMap::new();
terminal
.draw(|frame| {
let area = frame.area();
frame.render_widget(Paragraph::new("sentinel"), area);
render_session_or_diff_mode(
frame,
area,
&mode,
&sessions,
RouteAuxContext {
active_prompt_outputs: &HashMap::new(),
default_reasoning_level: ReasoningLevel::default(),
markdown_render_cache: &cache,
output_layout_cache: &output_layout_cache,
review_comment_cache: &review_comment_cache,
session_progress_messages: &progress_messages,
session_update_versions: &session_update_versions,
session_worktree_availability: &HashMap::new(),
wall_clock_unix_seconds: 0,
},
);
})
.expect("failed to draw");
let text = buffer_text(terminal.backend().buffer());
assert!(text.contains("sentinel"));
}
#[test]
fn render_session_or_diff_mode_renders_diff_page_for_matching_session() {
let backend = ratatui::backend::TestBackend::new(120, 30);
let mut terminal = ratatui::Terminal::new(backend).expect("failed to create terminal");
let session_id = "session-diff";
let mut session = session_fixture(session_id);
session.title = Some("Diff Session".to_string());
let sessions = vec![session];
let mode = AppMode::Diff {
diff: String::new(),
file_explorer_selected_index: 0,
restore_question: None,
right_panel: DiffRightPanel::Diff,
scroll_cache: None,
session_id: session_id.into(),
scroll_offset: 0,
};
let progress_messages = HashMap::new();
let cache = markdown::MarkdownRenderCache::default();
let review_comment_cache = ReviewCommentCache::default();
let output_layout_cache = component::session_output::SessionOutputLayoutCache::default();
let session_update_versions = HashMap::new();
terminal
.draw(|frame| {
render_session_or_diff_mode(
frame,
frame.area(),
&mode,
&sessions,
RouteAuxContext {
active_prompt_outputs: &HashMap::new(),
default_reasoning_level: ReasoningLevel::default(),
markdown_render_cache: &cache,
output_layout_cache: &output_layout_cache,
review_comment_cache: &review_comment_cache,
session_progress_messages: &progress_messages,
session_update_versions: &session_update_versions,
session_worktree_availability: &HashMap::new(),
wall_clock_unix_seconds: 0,
},
);
})
.expect("failed to draw");
let text = buffer_text(terminal.backend().buffer());
assert!(text.contains("Diff Session"));
assert!(text.contains("No changes found."));
}
#[test]
fn render_session_confirmation_overlay_renders_confirmation_text() {
let backend = ratatui::backend::TestBackend::new(120, 30);
let mut terminal = ratatui::Terminal::new(backend).expect("failed to create terminal");
let session_id = "session-merge";
let sessions = vec![session_fixture(session_id)];
let progress_messages = HashMap::new();
let confirmation_context = SessionConfirmationContext {
confirmation_message: "Queue merge now?",
confirmation_title: "Confirm Merge",
selected_confirmation_index: 0,
};
let view_mode = ConfirmationViewMode {
review_status_message: None,
review_text: None,
scroll_offset: None,
session_id: session_id.into(),
};
let cache = markdown::MarkdownRenderCache::default();
let output_layout_cache = component::session_output::SessionOutputLayoutCache::default();
let session_update_versions = HashMap::new();
terminal
.draw(|frame| {
render_session_confirmation_overlay(
frame,
frame.area(),
SessionOverlayRenderContext {
active_prompt_outputs: &HashMap::new(),
default_reasoning_level: ReasoningLevel::High,
markdown_render_cache: &cache,
output_layout_cache: &output_layout_cache,
restore_view: &view_mode,
session_progress_messages: &progress_messages,
session_update_versions: &session_update_versions,
session_worktree_availability: &HashMap::new(),
sessions: &sessions,
wall_clock_unix_seconds: 0,
},
&confirmation_context,
);
})
.expect("failed to draw");
let text = buffer_text(terminal.backend().buffer());
assert!(text.contains("Confirm Merge"));
assert!(text.contains("Queue merge now?"));
}
}