use ag_forge::RequestedReview;
use ratatui::layout::Rect;
use super::help_action::{self, HelpAction, ViewHelpState, ViewSessionState};
use super::prompt::{
PromptAtMentionState, PromptAttachmentState, PromptHistoryState, PromptSlashState,
};
use crate::domain::input::InputState;
use crate::domain::question::QuestionItem;
use crate::domain::session::{PublishBranchAction, SessionId};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfirmationIntent {
Quit,
CancelSession,
ContinueSession,
MergeSession,
RegenerateReview,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfirmationViewMode {
pub review_status_message: Option<String>,
pub review_text: Option<String>,
pub scroll_offset: Option<u16>,
pub session_id: SessionId,
}
impl ConfirmationViewMode {
#[must_use]
pub fn into_view_mode(self) -> AppMode {
AppMode::View {
review_status_message: self.review_status_message,
review_text: self.review_text,
session_id: self.session_id,
scroll_offset: self.scroll_offset,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiffScrollCache {
pub content_area: Rect,
pub file_explorer_selected_index: usize,
pub max_scroll_offset: u16,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum DiffRightPanel {
#[default]
Diff,
Comments,
}
impl DiffRightPanel {
#[must_use]
pub const fn toggled(self) -> Self {
match self {
Self::Diff => Self::Comments,
Self::Comments => Self::Diff,
}
}
}
pub struct QuestionModeSnapshot {
pub at_mention_state: Option<PromptAtMentionState>,
pub current_index: usize,
pub input: InputState,
pub questions: Vec<QuestionItem>,
pub review_status_message: Option<String>,
pub review_text: Option<String>,
pub responses: Vec<String>,
pub scroll_offset: Option<u16>,
pub selected_option_index: Option<usize>,
pub session_id: SessionId,
}
impl QuestionModeSnapshot {
#[must_use]
pub fn into_question_mode(self) -> AppMode {
AppMode::Question {
at_mention_state: self.at_mention_state,
current_index: self.current_index,
focus: QuestionFocus::Answer,
input: self.input,
questions: self.questions,
review_status_message: self.review_status_message,
review_text: self.review_text,
responses: self.responses,
scroll_offset: self.scroll_offset,
selected_option_index: self.selected_option_index,
session_id: self.session_id,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum QuestionFocus {
#[default]
Answer,
Chat,
}
pub enum AppMode {
List,
ReviewDetail {
review: RequestedReview,
scroll_offset: u16,
},
SessionCreation {
selected_option_index: usize,
},
Confirmation {
confirmation_intent: ConfirmationIntent,
confirmation_message: String,
confirmation_title: String,
restore_view: Option<ConfirmationViewMode>,
session_id: Option<SessionId>,
selected_confirmation_index: usize,
},
SyncBlockedPopup {
project_name: Option<String>,
default_branch: Option<String>,
is_loading: bool,
message: String,
title: String,
},
ViewInfoPopup {
is_loading: bool,
loading_label: String,
message: String,
restore_view: ConfirmationViewMode,
title: String,
},
OpenCommandSelector {
commands: Vec<String>,
restore_view: ConfirmationViewMode,
selected_command_index: usize,
},
PublishBranchInput {
default_branch_name: String,
input: InputState,
locked_upstream_ref: Option<String>,
publish_branch_action: PublishBranchAction,
restore_view: ConfirmationViewMode,
},
Prompt {
at_mention_state: Option<PromptAtMentionState>,
attachment_state: PromptAttachmentState,
history_state: PromptHistoryState,
review_status_message: Option<String>,
review_text: Option<String>,
slash_state: PromptSlashState,
session_id: SessionId,
input: InputState,
scroll_offset: Option<u16>,
},
View {
review_status_message: Option<String>,
review_text: Option<String>,
session_id: SessionId,
scroll_offset: Option<u16>,
},
Diff {
diff: String,
file_explorer_selected_index: usize,
restore_question: Option<QuestionModeSnapshot>,
right_panel: DiffRightPanel,
scroll_cache: Option<DiffScrollCache>,
scroll_offset: u16,
session_id: SessionId,
},
Question {
at_mention_state: Option<PromptAtMentionState>,
review_status_message: Option<String>,
review_text: Option<String>,
session_id: SessionId,
questions: Vec<QuestionItem>,
responses: Vec<String>,
current_index: usize,
focus: QuestionFocus,
input: InputState,
scroll_offset: Option<u16>,
selected_option_index: Option<usize>,
},
Help {
context: HelpContext,
scroll_offset: u16,
},
}
pub enum HelpContext {
List { keybindings: Vec<HelpAction> },
View {
can_open_worktree: bool,
review_status_message: Option<String>,
review_text: Option<String>,
publish_pull_request_action: Option<PublishBranchAction>,
session_id: SessionId,
session_state: ViewSessionState,
scroll_offset: Option<u16>,
},
Diff {
diff: String,
file_explorer_selected_index: usize,
restore_question: Option<QuestionModeSnapshot>,
right_panel: DiffRightPanel,
session_id: SessionId,
scroll_offset: u16,
},
}
impl HelpContext {
pub fn keybindings(&self) -> Vec<HelpAction> {
match self {
HelpContext::View {
can_open_worktree,
publish_pull_request_action,
session_state,
..
} => help_action::view_actions(ViewHelpState {
can_open_worktree: *can_open_worktree,
publish_pull_request_action: *publish_pull_request_action,
session_state: *session_state,
}),
HelpContext::List { keybindings } => keybindings.clone(),
HelpContext::Diff { right_panel, .. } => help_action::diff_actions(*right_panel),
}
}
pub fn restore_mode(self) -> AppMode {
match self {
HelpContext::List { .. } => AppMode::List,
HelpContext::View {
review_status_message,
review_text,
publish_pull_request_action: _,
session_id,
scroll_offset,
..
} => AppMode::View {
review_status_message,
review_text,
session_id,
scroll_offset,
},
HelpContext::Diff {
diff,
file_explorer_selected_index,
restore_question,
right_panel,
session_id,
scroll_offset,
} => AppMode::Diff {
diff,
file_explorer_selected_index,
restore_question,
right_panel,
scroll_cache: None,
session_id,
scroll_offset,
},
}
}
pub fn title(&self) -> &'static str {
"Keybindings"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::review_loading_message;
use crate::domain::agent::AgentModel;
use crate::domain::session::PublishBranchAction;
#[test]
fn test_confirmation_view_mode_into_view_mode_restores_snapshot_values() {
let confirmation_view_mode = ConfirmationViewMode {
review_status_message: Some(review_loading_message(AgentModel::Gpt55)),
review_text: Some("Critical finding".to_string()),
scroll_offset: Some(7),
session_id: "session-id".into(),
};
let mode = confirmation_view_mode.into_view_mode();
assert!(matches!(
mode,
AppMode::View {
review_status_message: Some(ref review_status_message),
review_text: Some(ref review_text),
ref session_id,
scroll_offset: Some(7),
} if session_id == "session-id"
&& review_status_message == &review_loading_message(AgentModel::Gpt55)
&& review_text == "Critical finding"
));
}
#[test]
fn test_help_context_view_keybindings_for_in_progress_hide_edit_actions() {
let context = HelpContext::View {
can_open_worktree: true,
review_status_message: None,
review_text: None,
publish_pull_request_action: None,
session_id: "session-id".into(),
session_state: ViewSessionState::InProgress,
scroll_offset: Some(2),
};
let bindings = context.keybindings();
assert!(bindings.iter().any(|binding| binding.key == "q"));
assert!(bindings.iter().any(|binding| binding.key == "j/k"));
assert!(bindings.iter().any(|binding| binding.key == "?"));
assert!(bindings.iter().any(|binding| binding.key == "Ctrl+c"));
assert!(!bindings.iter().any(|binding| binding.key == "Enter"));
assert!(!bindings.iter().any(|binding| binding.key == "d"));
assert!(!bindings.iter().any(|binding| binding.key == "m"));
assert!(!bindings.iter().any(|binding| binding.key == "r"));
assert!(!bindings.iter().any(|binding| binding.key == "S-Tab"));
}
#[test]
fn test_help_context_restore_mode_ignores_help_only_view_fields() {
let context = HelpContext::View {
can_open_worktree: true,
review_status_message: Some(review_loading_message(AgentModel::Gpt55)),
review_text: Some("Ready".to_string()),
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_id: "session-id".into(),
session_state: ViewSessionState::InProgress,
scroll_offset: Some(4),
};
let mode = context.restore_mode();
assert!(matches!(
mode,
AppMode::View {
ref session_id,
review_status_message: Some(ref review_status_message),
review_text: Some(ref review_text),
scroll_offset: Some(4),
..
} if session_id == "session-id"
&& review_status_message == &review_loading_message(AgentModel::Gpt55)
&& review_text == "Ready"
));
}
#[test]
fn test_help_context_view_keybindings_include_publish_pull_request_action() {
let context = HelpContext::View {
can_open_worktree: true,
review_status_message: None,
review_text: None,
publish_pull_request_action: Some(PublishBranchAction::PublishPullRequest),
session_id: "session-id".into(),
session_state: ViewSessionState::Interactive,
scroll_offset: None,
};
let bindings = context.keybindings();
assert!(bindings.iter().any(|binding| binding.key == "p"));
}
#[test]
fn test_help_context_list_keybindings_return_stored_actions() {
let keybindings = vec![
HelpAction::new("quit", "q", "Quit"),
HelpAction::new("help", "?", "Help"),
];
let context = HelpContext::List { keybindings };
let bindings = context.keybindings();
assert_eq!(bindings.len(), 2);
assert!(bindings.iter().any(|binding| binding.key == "q"));
assert!(bindings.iter().any(|binding| binding.key == "?"));
}
}