use super::super::*;
use super::display_reducer;
use super::{
MissionControlApp, PendingSessionSwitch,
actions::{ActionContext, AppControlFlow},
};
impl MissionControlApp {
pub(super) fn start_session_switch_worker(
&mut self,
session_id: String,
ui_state: &mut state::MissionControlState,
) {
self.start_session_switch_worker_with_admission(session_id, None, ui_state);
}
pub(super) fn start_session_switch_worker_with_admission(
&mut self,
session_id: String,
admitted_session: Option<Session>,
ui_state: &mut state::MissionControlState,
) {
if self.session_maintenance.is_pruning() {
ui_state.status = "session pruning in progress; please wait".into();
return;
}
if self.pending_export.is_some() {
ui_state.status = "session export is in progress; please wait".to_string();
return;
}
if self.pending_session_switch.is_some() {
ui_state.status = "session switch already loading".to_string();
return;
}
if self.active_run || self.worker.is_some() {
ui_state.status = "cannot switch sessions while a prompt is running".to_string();
return;
}
if self.state.active_session_id() == Some(session_id.as_str()) {
ui_state.close_session_picker();
ui_state.status = format!("session unchanged: {}", short_session_id(&session_id));
return;
}
self.next_session_switch_request_id = self.next_session_switch_request_id.saturating_add(1);
let request_id = self.next_session_switch_request_id;
self.pending_session_switch = Some(PendingSessionSwitch {
request_id,
session_id: session_id.clone(),
generation: self.session_generation,
});
ui_state.status = "loading session…".to_string();
let manager = self.state.session_manager.clone();
let config = self.state.config.clone();
let sender = self.events.clone();
thread::spawn(move || {
let result = match admitted_session {
Some(session) => crate::tui::sessions::commands::load_admitted_session_switch(
session,
config.as_ref(),
None,
)
.map_err(|error| error.to_string()),
None => start_session_switch_load(&manager, config.as_ref(), &session_id),
};
let _ = send_tui_event(
&sender,
TuiEvent::SessionSwitchLoaded {
request_id,
session_id,
result: Box::new(result),
},
);
});
}
fn apply_copy_request<C: input::clipboard::ClipboardWriter + ?Sized>(
text: &str,
state: &mut state::MissionControlState,
clipboard: &mut C,
now: Instant,
) -> bool {
if text.is_empty() {
return false;
}
if clipboard.copy(text).is_ok() {
state.show_copy_success_toast(now);
} else {
state.show_copy_failure_toast(now);
}
true
}
pub(super) fn reduce_input_command(
&mut self,
command: input::InputCommand,
ui_state: &mut state::MissionControlState,
context: &mut ActionContext<'_>,
prompt_revision_before: u64,
) -> (AppControlFlow, display_reducer::DisplayApplyResult) {
let (action, display) = command.into_parts();
if self.reject_exclusive_operation(&action, ui_state) {
return (
AppControlFlow::Continue,
display_reducer::DisplayApplyResult::default(),
);
}
let mut display_result =
display.map_or_else(display_reducer::DisplayApplyResult::default, |display| {
let mut result = display_reducer::apply_display_command(
display,
ui_state,
context.autocomplete_candidates,
);
if let Some(text) = result.copy.take() {
Self::apply_copy_request(&text, ui_state, context.clipboard, Instant::now());
}
result
});
self.mark_initial_prompt_input(
ui_state.prompt_content_revision() != prompt_revision_before,
Some(&action),
ui_state,
);
let flow = self.apply_input_action(action, ui_state, context);
display_result.copy = None;
(flow, display_result)
}
fn reject_exclusive_operation(
&self,
action: &input::InputAction,
ui_state: &mut state::MissionControlState,
) -> bool {
let reason = if self.pending_export.is_some() {
"session export is in progress; please wait"
} else if self
.pending_rewind
.as_ref()
.is_some_and(|pending| pending.kind == super::RewindRequestKind::Execute)
{
"rewind execution is still in progress; please wait"
} else if self.pending_model_selection.is_some() {
"model selection is still in progress; please wait"
} else {
return false;
};
if matches!(
action,
input::InputAction::Submit(_)
| input::InputAction::SelectModel(_)
| input::InputAction::SelectSession(_)
| input::InputAction::PreviewTheme(_)
| input::InputAction::ConfirmTheme
| input::InputAction::SetSkillEnabled { .. }
| input::InputAction::SetMcpServerEnabled { .. }
| input::InputAction::SetToolEnabled { .. }
| input::InputAction::SetSubagentProfileEnabled { .. }
| input::InputAction::SetModelEnabled { .. }
| input::InputAction::PrimaryAgentSelectionChanged(_)
| input::InputAction::ThinkingLevelSelectionChanged(_)
| input::InputAction::SelectConnectProvider(_)
| input::InputAction::SelectLogoutProvider(_)
| input::InputAction::ConfirmLogout(_)
| input::InputAction::ConfirmConnectProviderReplacement
| input::InputAction::SubmitConnectProviderStep
| input::InputAction::SubmitOAuthFallback
| input::InputAction::ConfirmRewind
| input::InputAction::OpenSetModel
) {
ui_state.status = reason.to_string();
return true;
}
false
}
pub(super) fn apply_input_action(
&mut self,
action: input::InputAction,
ui_state: &mut state::MissionControlState,
context: &mut ActionContext<'_>,
) -> AppControlFlow {
if self.reject_exclusive_operation(&action, ui_state) {
return AppControlFlow::Continue;
}
match action {
input::InputAction::Submit(prompt) => {
self.submit(prompt, ui_state, context.receiver, context.last_area);
}
input::InputAction::PreviewTheme(id) => self.preview_theme(&id, ui_state),
input::InputAction::ConfirmTheme => self.save_theme(ui_state),
input::InputAction::CancelTheme => self.cancel_theme(ui_state),
input::InputAction::SelectModel(model_id) => {
self.select_model(&model_id, ui_state);
}
input::InputAction::SelectSession(session_id) => {
self.start_session_switch_worker(session_id, ui_state);
}
input::InputAction::SetSkillEnabled { name, enabled } => {
self.set_skill_enabled(&name, enabled, ui_state);
}
input::InputAction::ToggleSkillsSettingsScope => {
self.toggle_skills_settings_scope(ui_state, context.last_area);
}
input::InputAction::SetMcpServerEnabled { name, enabled } => {
self.set_mcp_server_enabled(&name, enabled, ui_state);
}
input::InputAction::SetToolEnabled { name, enabled } => {
self.set_tool_enabled(&name, enabled, ui_state);
}
input::InputAction::ToggleToolsSettingsScope => {
self.toggle_tools_settings_scope(ui_state, context.last_area);
}
input::InputAction::SetSubagentProfileEnabled { id, enabled } => {
self.set_subagent_profile_enabled(&id, enabled, ui_state);
}
input::InputAction::ToggleSubagentsSettingsScope => {
self.toggle_subagents_settings_scope(ui_state, context.last_area);
}
input::InputAction::SetModelEnabled { id, enabled } => {
self.set_model_enabled(&id, enabled, ui_state);
}
input::InputAction::ToggleModelsSettingsScope => {
self.toggle_models_settings_scope(ui_state, context.last_area);
}
input::InputAction::PrimaryAgentSelectionChanged(selected_id) => {
self.persist_primary_agent_selection(selected_id.as_deref(), ui_state);
}
input::InputAction::ThinkingLevelSelectionChanged(level) => {
self.persist_thinking_level(level, ui_state);
}
input::InputAction::SelectConnectProvider(provider_id) => {
self.start_connect_provider(&provider_id, ui_state);
}
input::InputAction::SelectLogoutProvider(provider_id) => {
self.logout_provider(&provider_id, ui_state);
}
input::InputAction::MoveConnectProviderUp(visible_rows) => {
ui_state.connect_provider_up(visible_rows);
}
input::InputAction::MoveConnectProviderDown(visible_rows) => {
ui_state.connect_provider_down(visible_rows);
}
input::InputAction::FocusNextConnectProviderField => {
ui_state.connect_provider_next_field();
}
input::InputAction::FocusPreviousConnectProviderField => {
ui_state.connect_provider_previous_field();
}
input::InputAction::InsertConnectProviderChar(ch) => {
ui_state.insert_modal_field_char(ch);
}
input::InputAction::InsertConnectProviderText(text) => {
ui_state.insert_modal_field_text(&text.into_inner());
}
input::InputAction::BackspaceConnectProviderField => {
ui_state.backspace_modal_field();
}
input::InputAction::DeleteConnectProviderField => {
ui_state.delete_modal_field_forward();
}
input::InputAction::MoveConnectProviderFieldLeft => {
ui_state.move_modal_field_left();
}
input::InputAction::MoveConnectProviderFieldRight => {
ui_state.move_modal_field_right();
}
input::InputAction::MoveConnectProviderFieldHome => {
ui_state.move_modal_field_home();
}
input::InputAction::MoveConnectProviderFieldEnd => {
ui_state.move_modal_field_end();
}
input::InputAction::SubmitOAuthFallback => {
self.submit_connect_provider_fallback(ui_state);
}
input::InputAction::SubmitConnectProviderStep => {
self.submit_connect_provider_setup_step(ui_state);
}
input::InputAction::CopyOAuthUrl => {
let url = ui_state
.modals
.connect_provider
.as_ref()
.map(|connect| connect.url.clone());
match url.as_deref() {
Some(url) if !url.is_empty() => {
Self::apply_copy_request(url, ui_state, context.clipboard, Instant::now());
}
_ => ui_state.status = "OAuth URL is not ready yet".to_string(),
}
}
input::InputAction::CopySelectedPrompt(text) => {
Self::apply_copy_request(&text, ui_state, context.clipboard, Instant::now());
}
input::InputAction::OpenOAuthUrl => {
let url = ui_state
.modals
.connect_provider
.as_ref()
.map(|connect| connect.url.as_str());
ui_state.status = match url {
Some(url) if !url.is_empty() => match open_url_in_browser(url) {
Ok(()) => "opened OAuth URL in browser".to_string(),
Err(error) => {
format!("could not open browser: {error}; press [Ctrl-Y] to copy URL")
}
},
_ => "OAuth URL is not ready yet".to_string(),
};
}
input::InputAction::CancelConnectProvider => {
self.cancel_connect_provider(ui_state);
}
input::InputAction::RetryConnectProvider => {
self.retry_connect_provider(ui_state);
}
input::InputAction::BackConnectProvider => {
if ui_state.back_connect_provider() {
ui_state.status = "select provider".to_string();
}
}
input::InputAction::CloseConnectProvider => {
self.cancel_connect_provider(ui_state);
}
input::InputAction::ConfirmLogout(provider_id) => {
self.remove_logout_provider(&provider_id, ui_state);
}
input::InputAction::CancelLogout => {
ui_state.status = "logout cancelled; credentials unchanged".to_string();
}
input::InputAction::ConfirmConnectProviderReplacement => {
self.confirm_connect_provider_replacement(ui_state);
}
input::InputAction::CancelConnectProviderReplacement => {
if ui_state.modals.connect_provider.is_some() {
ui_state.back_connect_provider();
ui_state.status =
"custom provider replacement cancelled; edit the form or cancel"
.to_string();
}
}
input::InputAction::ConfirmRewind => {
if let Some((plan, mode)) = ui_state.take_rewind_plan() {
self.start_rewind_execution(plan, mode, ui_state);
}
}
input::InputAction::CancelRewind => {
ui_state.status = "rewind cancelled".to_string();
}
input::InputAction::CancelRunningPrompt => {
self.cancel_running_prompt(ui_state);
}
input::InputAction::CancelStartupPrompt => {
let was_queued = self.pending_startup_prompt.is_some();
self.clear_pending_startup_prompt();
ui_state.startup_prompt_queued = false;
if was_queued {
ui_state.focus_prompt();
ui_state.status = "startup prompt cancelled".to_string();
}
}
input::InputAction::OpenSetModel => {
if self.active_run || self.worker.is_some() {
ui_state.status = "cannot change model while a prompt is running".to_string();
} else {
self.open_model_picker(ui_state, context.last_area);
}
}
input::InputAction::Exit => {
self.clear_pending_startup_prompt();
ui_state.startup_prompt_queued = false;
if let Some(status) = self.request_cancel_active_worker(ui_state) {
ui_state.status = status;
}
return AppControlFlow::Exit;
}
input::InputAction::None => {}
}
AppControlFlow::Continue
}
}