use super::super::*;
use super::MissionControlApp;
use crate::{agent::steering::SteeringRejected, cancellation::is_run_canceled};
impl MissionControlApp {
fn restore_prompt_input(ui_state: &mut state::MissionControlState, prompt: String) {
ui_state.restore_rejected_prompt(prompt);
}
fn queue_startup_prompt(
&mut self,
prompt: String,
ui_state: &mut state::MissionControlState,
) -> bool {
self.initialize_startup_prompt_queue(true);
self.initial_prompt_flush_ack = None;
if let Some(pending) = self.pending_startup_prompt.as_mut() {
if pending.initial_candidate && pending.prompt == prompt {
pending.initial_candidate = false;
pending.painted = false;
self.pending_initial_prompt = None;
ui_state.clear_prompt_input();
ui_state.focus_prompt();
ui_state.startup_prompt_queued = true;
ui_state.status = "Prompt queued — waiting for startup".to_string();
ui_state.commit_prompt_submission();
return false;
}
if pending.initial_candidate {
self.cancel_initial_startup_prompt();
} else {
Self::restore_prompt_input(ui_state, prompt);
ui_state.startup_prompt_queued = true;
ui_state.status = "startup prompt already queued".to_string();
return false;
}
}
self.pending_startup_prompt = Some(super::PendingStartupPrompt {
prompt,
painted: false,
initial_candidate: false,
});
self.pending_initial_prompt = None;
ui_state.clear_prompt_input();
ui_state.focus_prompt();
ui_state.startup_prompt_queued = true;
ui_state.status = "Prompt queued — waiting for startup".to_string();
ui_state.commit_prompt_submission();
false
}
pub(crate) fn submit(
&mut self,
prompt: String,
ui_state: &mut state::MissionControlState,
_receiver: &Receiver<TuiEvent>,
terminal_area: ratatui::layout::Rect,
) -> bool {
if let TuiSubmitCommand::Summarize { enabled, arg } =
tui_submit_command(&prompt, &self.commands)
{
if arg.is_some() {
ui_state.status =
"usage: /summarize-start or /summarize-stop (no arguments)".into();
} else if self.state.current_session.is_none() {
ui_state.focus_summary();
ui_state.summary.error =
Some("Start a session before using summary controls.".into());
ui_state.status = "No active session to summarize".into();
} else {
self.poll_summarizer(ui_state);
ui_state.summary.requested_enabled = Some(enabled);
self.poll_summarizer(ui_state);
ui_state.focus_summary();
ui_state.status = if enabled {
"Summary start requested"
} else {
"Summary stop requested"
}
.into();
}
ui_state.commit_prompt_submission();
return false;
}
if self
.worker
.as_ref()
.is_some_and(|worker| worker.handle.is_finished())
{
let _ = self.finish_worker_if_ready(ui_state);
if self.worker.is_some() {
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "previous prompt worker still shutting down".to_string();
return false;
}
}
let command = tui_submit_command(&prompt, &self.commands);
if self.session_maintenance.is_pruning()
&& !matches!(&command, TuiSubmitCommand::Quit | TuiSubmitCommand::Help)
{
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "session pruning in progress; please wait".into();
return false;
}
if self.settings_persistence.is_pending()
&& !matches!(&command, TuiSubmitCommand::Quit | TuiSubmitCommand::Help)
{
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "settings persistence pending; action blocked".to_string();
return false;
}
if self.fast_mode_persistence.is_pending()
&& !matches!(
&command,
TuiSubmitCommand::Fast(crate::commands::FastModeCommand::Status)
| TuiSubmitCommand::FastUsage
| TuiSubmitCommand::Help
| TuiSubmitCommand::Quit
)
{
Self::restore_prompt_input(ui_state, prompt.clone());
ui_state.status = if matches!(&command, TuiSubmitCommand::Fast(_)) {
"fast mode persistence already in progress".to_string()
} else {
"fast mode persistence pending; action blocked".to_string()
};
return false;
}
if self.worker.is_some() && !self.active_run {
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "previous prompt worker still shutting down".to_string();
return false;
}
self.initialize_startup_prompt_queue(true);
let normal_prompt = matches!(command, TuiSubmitCommand::None) && !prompt.trim().is_empty();
if normal_prompt
&& !self.startup_prompt_launch_in_progress
&& self
.pending_startup_prompt
.as_ref()
.is_some_and(|pending| !pending.initial_candidate)
{
Self::restore_prompt_input(ui_state, prompt);
ui_state.startup_prompt_queued = true;
ui_state.status = "startup prompt already queued".to_string();
return false;
}
if self.pending_session_switch.is_some()
&& matches!(
command,
TuiSubmitCommand::None | TuiSubmitCommand::New | TuiSubmitCommand::PruneSessions(_)
)
{
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "session switch loading; action blocked".to_string();
return false;
}
if self.startup_readiness != StartupReadiness::Ready {
self.initial_prompt_flush_ack = None;
if self.startup_readiness == StartupReadiness::Failed {
self.clear_pending_startup_prompt();
Self::restore_prompt_input(ui_state, prompt);
ui_state.startup_prompt_queued = false;
ui_state.status = self
.startup_failure
.clone()
.unwrap_or_else(|| "Mission Control startup failed".to_string());
return false;
}
match &command {
TuiSubmitCommand::Help => {
self.cancel_initial_startup_prompt();
self.sync_startup_prompt_signal(ui_state);
}
TuiSubmitCommand::Quit => {
self.clear_pending_startup_prompt();
ui_state.startup_prompt_queued = false;
}
TuiSubmitCommand::None if prompt.trim().is_empty() => {
Self::restore_prompt_input(ui_state, prompt);
if self.pending_startup_prompt.is_some() {
ui_state.status = "startup prompt already queued".to_string();
}
return false;
}
TuiSubmitCommand::None => return self.queue_startup_prompt(prompt, ui_state),
_ => {
self.cancel_initial_startup_prompt();
Self::restore_prompt_input(ui_state, prompt);
self.sync_startup_prompt_signal(ui_state);
ui_state.status =
"startup policy still loading; prompt execution disabled".to_string();
return false;
}
}
}
if command.requires_idle() && (self.active_run || self.worker.is_some()) {
let active_run_error = command.active_run_error();
Self::restore_prompt_input(ui_state, prompt);
if let Some(message) = active_run_error {
Self::apply_ui_error(ui_state, message);
}
return false;
}
if !matches!(&command, TuiSubmitCommand::None) {
ui_state.commit_prompt_submission();
}
match command {
TuiSubmitCommand::Update(arg) => return self.request_update(arg, ui_state),
TuiSubmitCommand::Summarize { .. } => return false,
TuiSubmitCommand::Fast(command) => {
self.handle_fast_mode_command(command, ui_state);
return false;
}
TuiSubmitCommand::FastUsage => {
ui_state.status = crate::commands::FAST_MODE_USAGE.to_string();
return false;
}
TuiSubmitCommand::Help => {
ui_state.show_help = true;
ui_state.set_scroll_offset(&ui_state.scroll_views.help, 0);
ui_state.status = "showing help".to_string();
return false;
}
TuiSubmitCommand::Login(Some(provider_id)) => {
self.start_connect_provider(provider_id, ui_state);
return false;
}
TuiSubmitCommand::Login(None) => {
self.open_connect_provider(ui_state);
return false;
}
TuiSubmitCommand::Logout(Some(provider_id)) => {
self.logout_provider(provider_id, ui_state);
return false;
}
TuiSubmitCommand::Logout(None) => {
self.open_logout_picker(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Model(Some(model_id)) => {
self.select_model_from_text(model_id, ui_state);
return false;
}
TuiSubmitCommand::Model(None) => {
self.open_model_picker(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Models(Some(_)) => {
ui_state.status = "usage: /models".to_string();
return false;
}
TuiSubmitCommand::Models(None) => {
self.open_models_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Usage(Some(_)) => {
ui_state.status = "usage: /usage".to_string();
return false;
}
TuiSubmitCommand::Usage(None) => {
self.open_usage_modal(ui_state);
return false;
}
TuiSubmitCommand::Mcp(Some(_)) => {
ui_state.status = "usage: /mcp".to_string();
return false;
}
TuiSubmitCommand::Mcp(None) => {
self.open_mcp_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Tools(Some(_)) => {
ui_state.status = "usage: /tools".to_string();
return false;
}
TuiSubmitCommand::Tools(None) => {
self.open_tools_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Subagents(Some(_)) => {
ui_state.status = "usage: /subagents".to_string();
return false;
}
TuiSubmitCommand::Subagents(None) => {
self.open_subagents_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Skills => {
self.open_skills_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::Sessions(Some(_)) => {
ui_state.status = "usage: /sessions".to_string();
return false;
}
TuiSubmitCommand::Sessions(None) => {
self.open_sessions_modal(ui_state, terminal_area);
return false;
}
TuiSubmitCommand::SystemPrompt => {
self.start_system_prompt_preparation(ui_state);
return false;
}
TuiSubmitCommand::New => {
match execute_new_session_command(&mut self.state, ui_state) {
Ok(status) => {
self.session_generation = self.session_generation.saturating_add(1);
ui_state.status = status;
}
Err(error) => {
Self::apply_ui_error(ui_state, error);
}
}
return false;
}
TuiSubmitCommand::PruneSessions(arg) => {
self.start_session_maintenance(Some(arg.unwrap_or("").to_owned()), None, ui_state);
return false;
}
TuiSubmitCommand::Changes => {
self.start_rewind_changes(ui_state);
return false;
}
TuiSubmitCommand::Rewind(arg) => {
self.start_rewind_command(arg, ui_state);
return false;
}
TuiSubmitCommand::Export(arg) => {
self.start_export(arg, ui_state);
return false;
}
TuiSubmitCommand::Compact(arg) => {
self.start_compaction(arg.map(str::to_string), ui_state);
return false;
}
TuiSubmitCommand::Unsupported(command) => {
let message = format!("unknown command: {command}");
let _ = record_session_event(
self.state.current_session.as_ref(),
&self.state.cwd,
SessionEventKind::Diagnostic,
serde_json::json!({"level":"error", "message": message.clone()}),
);
Self::apply_ui_error(ui_state, message);
return false;
}
TuiSubmitCommand::Quit => return false,
TuiSubmitCommand::Theme(_) => {
self.open_theme_picker(ui_state);
return false;
}
TuiSubmitCommand::None => {}
}
match submit_decision(self.active_run, &prompt) {
SubmitDecision::Accept => {}
SubmitDecision::IgnoreEmpty => {
ui_state.commit_prompt_submission();
return false;
}
SubmitDecision::QueueSteering => {
let Some(worker) = self
.worker
.as_ref()
.filter(|worker| worker.accepts_steering)
else {
Self::restore_prompt_input(ui_state, prompt);
ui_state.status =
"active run cannot accept steering; wait for it to finish".to_string();
return false;
};
match worker.steering.try_enqueue(prompt.clone()) {
Ok(queued) => {
ui_state.clear_prompt_input();
ui_state.set_pending_steering_count(queued.pending_count);
self.last_steering_pending_count = queued.pending_count;
ui_state.status = "Steering queued".to_string();
ui_state.commit_prompt_submission();
return false;
}
Err(SteeringRejected::Full { capacity }) => {
let message = format!("Steering queue full ({capacity} max)");
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = message.clone();
Self::apply_ui_error(ui_state, message);
return false;
}
Err(SteeringRejected::Empty) => {
Self::restore_prompt_input(ui_state, prompt);
return false;
}
}
}
}
if self.worker.is_some() {
Self::restore_prompt_input(ui_state, prompt);
ui_state.status = "previous prompt worker still shutting down".to_string();
return false;
}
ui_state.session_usage.fold_completed_run();
let sender = self.events.clone();
let activity_namespace = next_activity_namespace();
let outcome = Arc::new(WorkerOutcomeState::default());
let usage_recorder = Arc::new(crate::tui::session_usage::SessionUsageRecorder::new(
self.state.current_session.clone(),
self.state.cwd.clone(),
Arc::clone(&outcome.usage),
));
let activity_sender: ActivitySender = Arc::new({
let sender = sender.clone();
let activity_namespace = activity_namespace.clone();
let usage_recorder = Arc::clone(&usage_recorder);
move |event| {
match usage_recorder.record_activity(&event).and_then(|event| {
if let Some(event) = event {
send_tui_event(&sender, TuiEvent::Activity(event))?;
}
Ok(())
}) {
Ok(()) => {}
Err(error) => {
let _ = sender.try_send(TuiEvent::Error(error.to_string()));
}
}
let event = events::namespace_activity_event(event, &activity_namespace);
if let Err(error) = send_tui_event(&sender, TuiEvent::Activity(event)) {
let _ = sender.try_send(TuiEvent::Error(error.to_string()));
}
}
});
let mut run_config = self
.state
.config
.clone()
.unwrap_or_else(|| self.config.clone());
run_config.model = Some(self.state.model.clone());
run_config.thinking_level = ui_state.thinking_level;
let is_bash_mode = crate::bash_mode::parse_bash_mode_prompt(&prompt).is_some();
let auth_snapshot = self.state.auth_state.clone();
let settings = self.settings.clone();
let session = self.state.current_session.clone();
let cwd = self.state.cwd.clone();
let instructions = self.instructions.clone();
let skills = self.skills.clone();
let selected_primary_agent = ui_state.selected_primary_agent_profile();
let disabled_tools = Arc::clone(&self.disabled_tools);
let disabled_subagent_profiles = Arc::clone(&self.disabled_subagent_profiles);
let subagent_profile_discovery = self.subagent_profile_discovery.clone();
let mcp = self.mcp.clone();
let herdr_reporter = self.state.herdr_reporter.clone();
#[cfg(test)]
let worker_start_gate = self.worker_start_gate.clone();
self.clear_pending_startup_prompt();
ui_state.startup_prompt_queued = false;
ui_state.start_running_prompt(prompt.clone());
ui_state.activity_motion.run_namespace = Some(activity_namespace.clone());
ui_state.commit_prompt_submission();
ui_state.status = if is_bash_mode {
"bash-mode: running command…".to_string()
} else {
"running prompt…".to_string()
};
self.active_run = true;
let cancel = Arc::new(AtomicBool::new(false));
ui_state.active_worker_id = Some(outcome.worker_id());
ui_state.last_run_finished_worker_id = None;
let worker_cancel = Arc::clone(&cancel);
let worker_outcome = Arc::clone(&outcome);
let steering = crate::agent::steering::AgentSteering::new();
self.last_steering_pending_count = 0;
let worker_steering = steering.clone();
self.steering = steering.clone();
let thread_steering = steering.clone();
let handle = thread::spawn(move || {
#[cfg(test)]
if let Some(gate) = worker_start_gate {
gate.wait();
}
let auth_state = if is_bash_mode || worker_cancel.load(Ordering::SeqCst) {
auth_snapshot
} else {
match crate::config::refreshed_codex_auth_state(&run_config.paths, &auth_snapshot) {
Ok(auth_state) => auth_state,
Err(_) if worker_cancel.load(Ordering::SeqCst) => {
crate::agent::report_preflight_cancelled(
crate::output::InvocationMode::MissionControl,
herdr_reporter.clone(),
);
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::RunCanceled {
prompt: prompt.clone(),
}),
);
return;
}
Err(error) => {
let message = codex_refresh_error_message(error);
let _ = record_session_event(
session.as_ref(),
&cwd,
SessionEventKind::Diagnostic,
serde_json::json!({"level":"error", "message": message}),
);
crate::agent::report_preflight_blocked(
crate::output::InvocationMode::MissionControl,
herdr_reporter.clone(),
);
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::Error(message)),
);
return;
}
}
};
if !is_bash_mode {
if !auth_state.is_ready() && !worker_cancel.load(Ordering::SeqCst) {
let message =
format!("provider not configured for '{}'", auth_state.provider());
let _ = record_session_event(
session.as_ref(),
&cwd,
SessionEventKind::Diagnostic,
serde_json::json!({"level":"error", "message": message}),
);
crate::agent::report_preflight_blocked(
crate::output::InvocationMode::MissionControl,
herdr_reporter.clone(),
);
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::Error(message)),
);
return;
}
run_config.auth = auth_state.credential().cloned();
}
let mut sink =
events::TuiOutputSink::new(sender.clone(), activity_sender, activity_namespace);
sink.usage_recorder = Some(usage_recorder);
let title_sender = sender.clone();
let title_notifier: crate::session_titles::SessionTitleNotifier =
Arc::new(move |update| {
let _ = send_critical(
&title_sender,
TuiEvent::SessionTitleUpdated {
session_id: update.session_id,
title: update.title,
},
);
});
let result = if is_bash_mode {
run_bash_mode_once(
&run_config,
&skills,
ProviderRunOptions {
settings: Some(settings.clone()),
prompt: &prompt,
session: session.as_ref(),
cwd: &cwd,
output_sink: Some(&mut sink),
selected_primary_agent: None,
cancellation: Some(Arc::clone(&worker_cancel)),
session_title_notifier: None,
herdr_reporter: herdr_reporter.clone(),
invocation_mode: crate::output::InvocationMode::MissionControl,
disabled_tools: Some(Arc::clone(&disabled_tools)),
disabled_subagent_profiles: Some(Arc::clone(&disabled_subagent_profiles)),
subagent_profile_discovery: Some(subagent_profile_discovery.clone()),
mcp: mcp.clone(),
},
)
} else {
run_provider_once_streaming_with_steering(
&run_config,
&instructions,
&skills,
ProviderRunOptions {
settings: Some(settings.clone()),
prompt: &prompt,
session: session.as_ref(),
cwd: &cwd,
output_sink: Some(&mut sink),
selected_primary_agent,
cancellation: Some(Arc::clone(&worker_cancel)),
session_title_notifier: Some(title_notifier),
herdr_reporter: herdr_reporter.clone(),
invocation_mode: crate::output::InvocationMode::MissionControl,
disabled_tools: Some(Arc::clone(&disabled_tools)),
disabled_subagent_profiles: Some(Arc::clone(&disabled_subagent_profiles)),
subagent_profile_discovery: Some(subagent_profile_discovery.clone()),
mcp: mcp.clone(),
},
thread_steering,
)
};
match result {
Ok(_) => {
send_completion(&sender, &worker_outcome, Some(WorkerFinalEvent::Done));
}
Err(error) if is_run_canceled(&error) => {
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::RunCanceled {
prompt: prompt.clone(),
}),
);
}
Err(error) => {
if !is_bash_mode {
let _ = record_session_event(
session.as_ref(),
&cwd,
SessionEventKind::Diagnostic,
serde_json::json!({"level":"error", "message": error.to_string()}),
);
}
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::Error(error.to_string())),
);
}
}
});
self.worker = Some(WorkerState {
handle,
cancel,
login_manual: None,
outcome,
outcome_reconciled: false,
shutdown_policy: WorkerShutdownPolicy::Cancel,
steering: worker_steering,
accepts_steering: !is_bash_mode,
});
true
}
}
#[cfg(test)]
mod system_prompt_tests {
use super::*;
#[test]
fn system_prompt_worker_ignores_closed_or_replaced_session_without_wake_delivery() {
for changed_session in [false, true] {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(1);
drop(receiver);
let mut app = super::super::tests::test_app(&temp, sender);
let mut ui = state::MissionControlState::default();
app.start_system_prompt_preparation(&mut ui);
assert!(ui.system_prompt_modal_visible());
app.start_system_prompt_preparation(&mut ui);
assert_eq!(ui.status, "system prompt preparation already in progress");
if changed_session {
app.session_generation += 1;
} else {
ui.close_system_prompt_modal();
}
ui.status = "newer action".into();
let deadline = Instant::now() + Duration::from_secs(10);
while app.system_prompt.is_pending() {
assert!(!app.reconcile_system_prompt_worker(&mut ui));
assert!(
Instant::now() < deadline,
"system prompt worker did not finish"
);
thread::sleep(Duration::from_millis(1));
}
assert_eq!(ui.status, "newer action");
assert_eq!(ui.system_prompt_modal_visible(), changed_session);
}
}
#[test]
fn system_prompt_worker_publishes_prompt_without_wake_delivery() {
for disconnected in [false, true] {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(1);
sender.send(TuiEvent::WorkerOutcomeReady).unwrap();
let receiver = if disconnected { None } else { Some(receiver) };
let mut app = super::super::tests::test_app(&temp, sender);
let mut ui = state::MissionControlState::default();
app.start_system_prompt_preparation(&mut ui);
let deadline = Instant::now() + Duration::from_secs(10);
while !app.reconcile_system_prompt_worker(&mut ui) {
assert!(Instant::now() < deadline, "system prompt result was lost");
thread::sleep(Duration::from_millis(1));
}
assert_eq!(ui.status, "showing current computed system prompt");
assert!(ui.system_prompt_modal_visible());
app.cleanup_after_run().unwrap();
assert!(!app.system_prompt.is_pending());
drop(receiver);
}
}
}