use std::collections::HashMap;
use std::path::Path;
use crate::app::session_state::SessionGitStatus;
use crate::app::{App, Tab, UpdateStatus, session};
use crate::domain::agent::{AgentCliInfo, ReasoningLevel};
use crate::domain::project::ProjectListItem;
use crate::domain::session::{DailyActivity, Session, SessionId};
use crate::domain::theme::ColorTheme;
use crate::infra::clock;
use crate::presentation::app_mode::{AppMode, HelpContext};
use crate::presentation::frame_time::FrameTime;
use crate::presentation::settings::SettingsScreenSnapshot;
pub(crate) struct SessionReviewView<'a> {
pub(crate) session_id: &'a str,
pub(crate) text: Option<&'a str>,
}
pub(crate) struct AppViewSnapshot<'a> {
pub(crate) active_project_id: i64,
pub(crate) active_prompt_outputs: &'a HashMap<SessionId, String>,
pub(crate) available_agent_clis: Vec<AgentCliInfo>,
pub(crate) current_tab: Tab,
pub(crate) default_reasoning_level: ReasoningLevel,
pub(crate) frame_time: FrameTime,
pub(crate) git_branch: Option<&'a str>,
pub(crate) git_status: Option<(u32, u32)>,
pub(crate) git_upstream_ref: Option<&'a str>,
pub(crate) is_tmux_session: bool,
pub(crate) latest_available_version: Option<&'a str>,
pub(crate) mode: &'a AppMode,
pub(crate) mru_project_order: &'a [usize],
pub(crate) project_selected_index: Option<usize>,
pub(crate) projects: &'a [ProjectListItem],
pub(crate) session_branch_names: &'a HashMap<SessionId, String>,
pub(crate) session_git_statuses: &'a HashMap<SessionId, SessionGitStatus>,
pub(crate) session_index_by_id: &'a HashMap<SessionId, usize>,
pub(crate) session_progress_messages: &'a HashMap<SessionId, String>,
pub(crate) session_review: Option<SessionReviewView<'a>>,
pub(crate) session_selected_index: Option<usize>,
pub(crate) session_update_versions: &'a HashMap<SessionId, u64>,
pub(crate) session_worktree_availability: &'a HashMap<SessionId, bool>,
pub(crate) sessions: &'a [Session],
pub(crate) settings_screen: Option<SettingsScreenSnapshot>,
pub(crate) stats_activity: &'a [DailyActivity],
pub(crate) status_bar_fyi_rotation_index: u64,
pub(crate) theme: ColorTheme,
pub(crate) update_status: Option<&'a UpdateStatus>,
pub(crate) working_dir: &'a Path,
}
impl App {
pub(crate) fn view_snapshot(&self) -> AppViewSnapshot<'_> {
let clock_client = self.services.clock();
let system_time = clock_client.now_system_time();
let wall_clock_unix_seconds = session::unix_timestamp_from_system_time(system_time);
let frame_time = FrameTime::new(
wall_clock_unix_seconds,
clock::unix_timestamp_millis(system_time),
clock_client.local_utc_offset_seconds(wall_clock_unix_seconds),
);
let status_bar_fyi_rotation_index =
u64::try_from(wall_clock_unix_seconds.div_euclid(60)).unwrap_or_default();
let visible_session_id = visible_review_session_id(&self.mode);
let session_review = visible_session_id.map(|session_id| {
let (_, text) = self.review_view_state(session_id);
SessionReviewView { session_id, text }
});
let project = self.projects.render_parts();
let sessions = self.sessions.render_parts();
let current_tab = self.tabs.current();
let settings_screen = (current_tab == Tab::Settings)
.then(|| self.settings_presentation.snapshot(&self.settings.view()));
AppViewSnapshot {
active_project_id: project.active_project_id,
active_prompt_outputs: sessions.active_prompt_outputs,
available_agent_clis: self.services.available_agent_clis(),
current_tab,
default_reasoning_level: self.settings.default_smart_reasoning_level,
frame_time,
git_branch: project.git_branch,
git_status: project.git_status,
git_upstream_ref: project.git_upstream_ref,
is_tmux_session: self.is_tmux_session(),
latest_available_version: self.latest_available_version.as_deref(),
mode: &self.mode,
mru_project_order: project.mru_project_order,
project_selected_index: project.selected_index,
projects: project.project_items,
session_branch_names: sessions.session_branch_names,
session_git_statuses: sessions.session_git_statuses,
session_index_by_id: sessions.session_index_by_id,
session_progress_messages: &self.session_progress_messages,
session_review,
session_selected_index: sessions.selected_index,
session_update_versions: &self.last_seen_session_update_versions,
session_worktree_availability: sessions.session_worktree_availability,
sessions: sessions.sessions,
settings_screen,
stats_activity: sessions.stats_activity,
status_bar_fyi_rotation_index,
theme: self.settings.theme,
update_status: self.update_status.as_ref(),
working_dir: project.working_dir,
}
}
}
fn visible_review_session_id(mode: &AppMode) -> Option<&str> {
match mode {
AppMode::View { session_id, .. }
| AppMode::Prompt { session_id, .. }
| AppMode::Question { session_id, .. }
| AppMode::DiffLoading { session_id, .. }
| AppMode::Diff { session_id, .. }
| AppMode::Help {
context: HelpContext::View { session_id, .. } | HelpContext::Diff { session_id, .. },
..
} => Some(session_id),
AppMode::Confirmation {
restore_view: Some(restore_view),
..
}
| AppMode::LaunchConfigurationSelector { restore_view, .. }
| AppMode::PublishBranchInput { restore_view, .. }
| AppMode::ViewInfoPopup { restore_view, .. } => Some(&restore_view.session_id),
AppMode::List
| AppMode::SessionCreation { .. }
| AppMode::PreCommitHookWarning { .. }
| AppMode::ProjectSwitcher { .. }
| AppMode::Confirmation { .. }
| AppMode::SyncBlockedPopup { .. }
| AppMode::Help { .. } => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::presentation::app_mode::{DiffFocus, DiffLineComments};
#[test]
fn visible_review_session_id_includes_diff_comments() {
let mode = AppMode::Diff {
diff: String::new(),
file_explorer_selected_index: 0,
focus: DiffFocus::Files,
line_comments: DiffLineComments::default(),
selected_diff_line_index: 0,
preview: crate::presentation::app_mode::DiffPreview::default(),
review_comments: Some(crate::presentation::app_mode::DiffReviewComments::loading(
1,
)),
restore: None,
scroll_cache: None,
session_id: "session-id".into(),
scroll_offset: 0,
};
let session_id = visible_review_session_id(&mode);
assert_eq!(session_id, Some("session-id"));
}
#[test]
fn visible_review_session_id_includes_loading_diff() {
let mode = AppMode::DiffLoading {
fallback_view_scroll_offset: None,
request_id: 1,
restore: None,
session_id: "loading-session".into(),
sidebar_focus: crate::presentation::app_mode::DiffSidebarFocus::Files,
};
let session_id = visible_review_session_id(&mode);
assert_eq!(session_id, Some("loading-session"));
}
#[tokio::test]
async fn view_snapshot_builds_settings_screen_only_for_settings_tab() {
let (mut app, _base_dir) = crate::test_support::new_test_app().await;
app.tabs.set(Tab::Sessions);
let sessions_tab_has_settings_screen = app.view_snapshot().settings_screen.is_some();
app.tabs.set(Tab::Settings);
let settings_tab_has_settings_screen = app.view_snapshot().settings_screen.is_some();
assert!(!sessions_tab_has_settings_screen);
assert!(settings_tab_has_settings_screen);
}
}