use super::super::*;
use super::MissionControlApp;
use crate::agent::{cancellation::is_run_canceled, steering::SteeringRejected};
impl MissionControlApp {
pub(crate) fn submit(
&mut self,
prompt: String,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
terminal_area: ratatui::layout::Rect,
) -> bool {
if self
.worker
.as_ref()
.is_some_and(|worker| worker.handle.is_finished())
{
let drain_result = drain_tui_events(receiver, ui_state);
if drain_result.run_finished {
self.join_completed_worker(ui_state);
}
if self
.worker
.as_ref()
.is_some_and(|worker| worker.handle.is_finished())
{
self.finish_worker_if_ready(ui_state);
}
}
let command = tui_submit_command(&prompt, &self.commands);
if self.pending_session_switch.is_some()
&& matches!(
command,
TuiSubmitCommand::None | TuiSubmitCommand::New | TuiSubmitCommand::PruneSessions(_)
)
{
ui_state.input = prompt;
ui_state.prompt_cursor = ui_state.input.len();
ui_state.set_scroll_offset(&ui_state.scroll_views.prompt, 0);
ui_state.prompt_target_column = None;
ui_state.focus_prompt();
ui_state.status = "session switch loading; action blocked".to_string();
return false;
}
if !self.startup_policy_loaded
&& !matches!(command, TuiSubmitCommand::Help | TuiSubmitCommand::Quit)
{
ui_state.input = prompt;
ui_state.prompt_cursor = ui_state.input.len();
ui_state.set_scroll_offset(&ui_state.scroll_views.prompt, 0);
ui_state.prompt_target_column = None;
ui_state.focus_prompt();
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()) {
if let Some(message) = command.active_run_error() {
let _ = send_critical(&self.events, TuiEvent::Error(message.to_string()));
}
return false;
}
match command {
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_login(provider_id, ui_state);
return false;
}
TuiSubmitCommand::Login(None) => {
self.open_login_picker(ui_state, terminal_area);
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 => {
let mut run_config = self
.state
.config
.clone()
.unwrap_or_else(|| self.config.clone());
run_config.model = Some(self.state.model.clone());
let disabled_tools = self
.disabled_tools
.lock()
.map(|disabled| disabled.clone())
.unwrap_or_else(|_| {
crate::tools::MVP_TOOL_CAPABILITIES
.iter()
.map(|tool| tool.canonical_name().to_string())
.collect()
});
let disabled_subagent_profiles = self
.disabled_subagent_profiles
.lock()
.map(|disabled| disabled.clone());
let subagents_section = match disabled_subagent_profiles {
Ok(disabled_subagent_profiles) => {
let discovery = crate::subagents::profiles::filter_enabled_profiles(
&self.subagent_profile_discovery,
&disabled_subagent_profiles,
);
if disabled_tools.contains(crate::tools::contract::tool_name::SUBAGENTS) {
Ok(None)
} else {
crate::subagents::profiles::render_subagent_profiles_prompt(
Some(&run_config.paths.prompts),
&discovery,
)
}
}
Err(_) => Err(anyhow::anyhow!("disabled subagent profiles lock poisoned")),
};
let agent = subagents_section.and_then(|subagents_section| {
crate::agent::AgentSession::new_with_prompt_dir_and_subagents_and_disabled(
run_config
.model
.clone()
.unwrap_or_else(|| crate::providers::DEFAULT_CODEX_MODEL.to_string()),
Some(&run_config.paths.prompts),
&self.instructions,
&self.skills,
subagents_section.as_deref(),
&disabled_tools,
)
});
match agent {
Ok(agent) => {
let agent = append_primary_agent_to_main_prompt(
agent,
ui_state.selected_primary_agent_profile().as_ref(),
);
let prompt = agent.system_prompt();
let provider_id = run_config.provider_id();
let model = run_config.model.as_deref().unwrap_or_else(|| {
crate::providers::default_model_for_provider(provider_id)
});
let count = crate::context::project_text_tokens(provider_id, model, prompt);
let max_tokens = crate::config::load_context_budget(&run_config)
.unwrap_or_default()
.max_tokens;
let percent = count.tokens * 100 / max_tokens.max(1);
ui_state.open_system_prompt_modal(format!(
"~{} tokens / {} max ({}%)\n\n{}",
count.tokens, max_tokens, percent, prompt
));
ui_state.status = "showing current computed system prompt".to_string();
}
Err(error) => {
ui_state.open_system_prompt_modal(format!(
"Failed to compute system prompt: {error}"
));
ui_state.status = "failed to compute system prompt".to_string();
}
}
return false;
}
TuiSubmitCommand::New => {
let _ = drain_stale_tui_events(receiver);
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) => {
let _ = send_critical(&self.events, TuiEvent::Error(error.to_string()));
}
}
return false;
}
TuiSubmitCommand::PruneSessions(arg) => {
match execute_prune_sessions_command(&mut self.state, ui_state, arg) {
Ok(status) => {
ui_state.status = status;
self.request_selected_session_preview(ui_state);
}
Err(error) => ui_state.status = error.to_string(),
}
return false;
}
TuiSubmitCommand::Changes => {
let store = crate::checkpoints::CheckpointStore::from_paths(&self.config.paths);
if let Some(session_id) = self.state.active_session_id() {
let text = crate::shell::format_changes_for_tui(&store.changes(session_id));
ui_state.open_rewind_modal(text, None, true);
ui_state.status = "showing rewindable changes".to_string();
} else {
ui_state.status = "/changes requires an active persisted session".to_string();
}
return false;
}
TuiSubmitCommand::Rewind(arg) => {
let store = crate::checkpoints::CheckpointStore::from_paths(&self.config.paths);
let Some(session_id) = self.state.active_session_id() else {
ui_state.status = "/rewind requires an active persisted session".to_string();
return false;
};
match crate::checkpoints::parse_rewind_target(arg) {
Ok(crate::checkpoints::ParsedRewindTarget::NeedsSelection) => {
let changes = store.changes(session_id);
let plans = changes
.turns
.iter()
.filter(|turn| turn.changes.iter().any(|change| change.rewindable))
.map(|turn| {
store.plan_rewind(session_id, &self.state.cwd, turn.user_turn)
})
.collect::<Vec<_>>();
if plans.is_empty() {
ui_state.open_rewind_modal(
crate::shell::format_changes_for_tui(&changes),
None,
true,
);
ui_state.status = "no rewindable file changes".to_string();
} else {
ui_state.open_rewind_picker(plans);
ui_state.status =
"select rewind target; press Enter to apply".to_string();
}
}
Ok(crate::checkpoints::ParsedRewindTarget::Target {
target_turn,
dry_run,
}) => {
let plan = store.plan_rewind(session_id, &self.state.cwd, target_turn);
ui_state.open_rewind_modal(
crate::shell::format_rewind_plan_for_tui(&plan),
(!dry_run).then_some(plan),
dry_run,
);
ui_state.status = if dry_run {
"showing rewind dry-run".to_string()
} else {
"previewing rewind; press Enter to apply".to_string()
};
}
Err(error) => {
ui_state.status = format!("usage: /rewind [--dry-run] --to <turn>; {error}")
}
}
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}),
);
let _ = send_critical(&self.events, TuiEvent::Error(message));
return false;
}
TuiSubmitCommand::Quit => return false,
TuiSubmitCommand::None => {}
}
match submit_decision(self.active_run, &prompt) {
SubmitDecision::Accept => {}
SubmitDecision::IgnoreEmpty => return false,
SubmitDecision::QueueSteering => {
let Some(worker) = self
.worker
.as_ref()
.filter(|worker| worker.accepts_steering)
else {
ui_state.input = prompt;
ui_state.prompt_cursor = ui_state.input.len();
ui_state.set_scroll_offset(&ui_state.scroll_views.prompt, 0);
ui_state.prompt_target_column = None;
ui_state.focus_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();
return false;
}
Err(SteeringRejected::Full { capacity }) => {
let message = format!("Steering queue full ({capacity} max)");
ui_state.input = prompt;
ui_state.prompt_cursor = ui_state.input.len();
ui_state.set_scroll_offset(&ui_state.scroll_views.prompt, 0);
ui_state.prompt_target_column = None;
ui_state.focus_prompt();
ui_state.status = message.clone();
let _ = send_critical(&self.events, TuiEvent::Error(message));
return false;
}
Err(SteeringRejected::Empty) => return false,
}
}
}
if self.worker.is_some() {
let _ = send_critical(
&self.events,
TuiEvent::Error("previous prompt worker still shutting down".to_string()),
);
return false;
}
let sender = self.events.clone();
let activity_namespace = next_activity_namespace();
let activity_sender: ActivitySender = Arc::new({
let sender = sender.clone();
let activity_namespace = activity_namespace.clone();
move |event| {
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_state = if is_bash_mode {
self.state.auth_state.clone()
} else {
match crate::login::refreshed_auth_state(&run_config.paths, &self.state.auth_state) {
Ok(auth_state) => auth_state,
Err(error) => {
ui_state.provider_ready = false;
let message = codex_refresh_error_message(error);
let _ = send_critical(&sender, TuiEvent::Error(message));
return false;
}
}
};
if !is_bash_mode {
if auth_state.is_ready() {
self.state.auth_state = auth_state.clone();
ui_state.provider_ready = true;
run_config.auth = auth_state.credential().cloned();
} else {
ui_state.provider_ready = false;
}
}
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();
if !is_bash_mode && !auth_state.is_ready() {
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}),
);
let _ = send_critical(&sender, TuiEvent::Error(message));
return false;
}
ui_state.start_running_prompt(prompt.clone());
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));
let outcome = Arc::new(WorkerOutcomeState::default());
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 || {
if worker_cancel.load(Ordering::SeqCst) {
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::RunCanceled {
prompt: prompt.clone(),
}),
);
return;
}
let mut sink =
events::TuiOutputSink::new(sender.clone(), activity_sender, activity_namespace);
let title_sender = sender.clone();
let title_notifier: crate::sessions::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: None,
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,
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: None,
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),
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,
)
};
if worker_cancel.load(Ordering::SeqCst) {
send_completion(
&sender,
&worker_outcome,
Some(WorkerFinalEvent::RunCanceled {
prompt: prompt.clone(),
}),
);
return;
}
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,
steering: worker_steering,
accepts_steering: !is_bash_mode,
});
true
}
}