use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::mpsc;
use super::core::AppEvent;
use super::task;
use crate::app::session_state::SessionState;
use crate::domain::agent::AgentModel;
use crate::domain::session::Status;
use crate::infra::git::GitClient;
use crate::ui::state::app_mode::{AppMode, ConfirmationViewMode, HelpContext};
#[derive(Debug)]
pub(crate) enum ReviewCacheEntry {
Loading {
diff_hash: u64,
},
Ready {
diff_hash: u64,
text: String,
},
Failed {
diff_hash: u64,
error: String,
},
}
impl ReviewCacheEntry {
pub(crate) fn diff_hash(&self) -> u64 {
match self {
Self::Loading { diff_hash }
| Self::Ready { diff_hash, .. }
| Self::Failed { diff_hash, .. } => *diff_hash,
}
}
pub(crate) fn from_result(diff_hash: u64, result: &Result<String, String>) -> Self {
match result {
Ok(review_text) => Self::Ready {
diff_hash,
text: review_text.clone(),
},
Err(error) => Self::Failed {
diff_hash,
error: error.clone(),
},
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ReviewUpdate {
pub(crate) diff_hash: u64,
pub(crate) result: Result<String, String>,
}
const REVIEW_LOADING_MESSAGE_PREFIX: &str = "Reviewing changes with";
struct ReviewModeTarget<'a> {
review_status_message: &'a mut Option<String>,
review_text: &'a mut Option<String>,
}
pub(crate) fn diff_content_hash(diff: &str) -> u64 {
let mut hasher = DefaultHasher::new();
diff.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn is_review_loading_status_message(status_message: &str) -> bool {
status_message.starts_with(REVIEW_LOADING_MESSAGE_PREFIX)
}
pub(crate) fn review_loading_message(review_model: AgentModel) -> String {
format!("{REVIEW_LOADING_MESSAGE_PREFIX} {}", review_model.as_str())
}
pub(crate) fn review_view_state(
review_cache: &HashMap<String, ReviewCacheEntry>,
session_id: &str,
review_model: AgentModel,
) -> (Option<String>, Option<String>) {
let Some(cache_entry) = review_cache.get(session_id) else {
return (None, None);
};
match cache_entry {
ReviewCacheEntry::Loading { .. } => (Some(review_loading_message(review_model)), None),
ReviewCacheEntry::Ready { text, .. } => (None, Some(text.clone())),
ReviewCacheEntry::Failed { error, .. } => (
Some(format!("Review assist unavailable: {}", error.trim())),
None,
),
}
}
pub(crate) fn start_review_assist(
app_event_tx: mpsc::UnboundedSender<AppEvent>,
review_model: AgentModel,
session_id: &str,
session_folder: &Path,
diff_hash: u64,
review_diff: &str,
session_summary: Option<&str>,
) {
task::TaskService::spawn_review_assist_task(task::ReviewAssistTaskInput {
app_event_tx,
diff_hash,
review_diff: review_diff.to_string(),
session_folder: session_folder.to_path_buf(),
session_id: session_id.to_string(),
review_model,
session_summary: session_summary.map(str::to_string),
});
}
pub(crate) fn mark_session_agent_review(session_state: &mut SessionState, session_id: &str) {
update_transient_review_status(
session_state,
session_id,
Status::Review,
Status::AgentReview,
);
}
pub(crate) fn apply_review_updates(
review_cache: &mut HashMap<String, ReviewCacheEntry>,
mode: &mut AppMode,
session_state: &mut SessionState,
review_updates: HashMap<String, ReviewUpdate>,
) {
for (session_id, review_update) in review_updates {
apply_review_update(
review_cache,
mode,
session_state,
&session_id,
review_update,
);
}
}
pub(crate) async fn auto_start_reviews(
review_cache: &mut HashMap<String, ReviewCacheEntry>,
session_ids: &HashSet<String>,
session_state: &mut SessionState,
git_client: Arc<dyn GitClient>,
app_event_tx: mpsc::UnboundedSender<AppEvent>,
review_model: AgentModel,
) {
for session_id in session_ids {
let Some((current_status, session_folder, base_branch, session_summary)) = session_state
.sessions
.iter()
.find(|session| session.id == *session_id)
.map(|session| {
(
session.status,
session.folder.clone(),
session.base_branch.clone(),
session.summary.clone(),
)
})
else {
continue;
};
if current_status == Status::InProgress {
review_cache.remove(session_id);
continue;
}
if current_status != Status::Review {
continue;
}
let diff = git_client
.diff(session_folder.clone(), base_branch)
.await
.unwrap_or_default();
if diff.trim().is_empty() || diff.starts_with("Failed to run git diff:") {
continue;
}
let new_hash = diff_content_hash(&diff);
if review_cache
.get(session_id)
.is_some_and(|entry| entry.diff_hash() == new_hash)
{
continue;
}
review_cache.insert(
session_id.clone(),
ReviewCacheEntry::Loading {
diff_hash: new_hash,
},
);
mark_session_agent_review(session_state, session_id);
start_review_assist(
app_event_tx.clone(),
review_model,
session_id,
&session_folder,
new_hash,
&diff,
session_summary.as_deref(),
);
}
}
fn apply_review_update(
review_cache: &mut HashMap<String, ReviewCacheEntry>,
mode: &mut AppMode,
session_state: &mut SessionState,
session_id: &str,
review_update: ReviewUpdate,
) {
let ReviewUpdate { diff_hash, result } = review_update;
let Some(cache_entry) = review_cache.get(session_id) else {
return;
};
if cache_entry.diff_hash() != diff_hash {
return;
}
review_cache.insert(
session_id.to_string(),
ReviewCacheEntry::from_result(diff_hash, &result),
);
restore_session_review_status(session_state, session_id);
if let Some(mode_target) = review_mode_target(mode, session_id) {
apply_review_result(
mode_target.review_status_message,
mode_target.review_text,
result,
);
}
}
fn restore_session_review_status(session_state: &mut SessionState, session_id: &str) {
update_transient_review_status(
session_state,
session_id,
Status::AgentReview,
Status::Review,
);
}
fn update_transient_review_status(
session_state: &mut SessionState,
session_id: &str,
current_status: Status,
next_status: Status,
) {
if let Some(session) = session_state
.sessions
.iter_mut()
.find(|session| session.id == session_id)
&& session.status == current_status
{
session.status = next_status;
}
if let Some(handles) = session_state.handles.get(session_id)
&& let Ok(mut handle_status) = handles.status.lock()
&& *handle_status == current_status
{
*handle_status = next_status;
}
}
fn review_mode_target<'a>(mode: &'a mut AppMode, session_id: &str) -> Option<ReviewModeTarget<'a>> {
match mode {
AppMode::View {
review_status_message,
review_text,
session_id: view_session_id,
..
} if view_session_id == session_id => Some(ReviewModeTarget {
review_status_message,
review_text,
}),
AppMode::Help {
context:
HelpContext::View {
review_status_message,
review_text,
session_id: view_session_id,
..
},
..
} if view_session_id == session_id => Some(ReviewModeTarget {
review_status_message,
review_text,
}),
AppMode::OpenCommandSelector { restore_view, .. }
| AppMode::PublishBranchInput { restore_view, .. }
| AppMode::ViewInfoPopup { restore_view, .. } => {
confirmation_review_mode_target(restore_view, session_id)
}
AppMode::List
| AppMode::Confirmation { .. }
| AppMode::SyncBlockedPopup { .. }
| AppMode::Prompt { .. }
| AppMode::Question { .. }
| AppMode::Diff { .. }
| AppMode::Help { .. }
| AppMode::View { .. } => None,
}
}
fn confirmation_review_mode_target<'a>(
restore_view: &'a mut ConfirmationViewMode,
session_id: &str,
) -> Option<ReviewModeTarget<'a>> {
if restore_view.session_id != session_id {
return None;
}
Some(ReviewModeTarget {
review_status_message: &mut restore_view.review_status_message,
review_text: &mut restore_view.review_text,
})
}
fn apply_review_result(
review_status_message: &mut Option<String>,
review_text: &mut Option<String>,
result: Result<String, String>,
) {
match result {
Ok(text) => {
*review_status_message = None;
*review_text = Some(text);
}
Err(error) => {
*review_status_message = Some(format!("Review assist unavailable: {}", error.trim()));
*review_text = None;
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
fn review_loading_message_uses_requested_model_name() {
let review_model = AgentModel::Gpt54;
let message = review_loading_message(review_model);
assert_eq!(message, "Reviewing changes with gpt-5.4");
}
#[test]
fn is_review_loading_status_message_matches_model_aware_copy() {
let status_message = review_loading_message(AgentModel::ClaudeOpus46);
let is_loading = is_review_loading_status_message(&status_message);
assert!(is_loading);
}
#[test]
fn review_view_state_uses_loading_message_for_cached_review_generation() {
let mut review_cache = HashMap::new();
review_cache.insert(
"session-id".to_string(),
ReviewCacheEntry::Loading { diff_hash: 7 },
);
let (review_status_message, review_text) =
review_view_state(&review_cache, "session-id", AgentModel::ClaudeSonnet46);
assert_eq!(
review_status_message.as_deref(),
Some("Reviewing changes with claude-sonnet-4-6")
);
assert_eq!(review_text, None);
}
}