use super::super::*;
use super::{MissionControlApp, actions};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct PendingStartupPrompt {
pub(super) prompt: String,
pub(super) painted: bool,
pub(super) initial_candidate: bool,
}
const DECORATIVE_STARTUP_JOIN_GRACE_PERIOD: Duration = Duration::from_millis(50);
fn join_decorative_startup_worker(worker: JoinHandle<()>) -> bool {
matches!(
crate::thread_join::join_with_timeout(worker, DECORATIVE_STARTUP_JOIN_GRACE_PERIOD),
Ok(Err(_))
)
}
fn join_critical_or_provider_worker(worker: JoinHandle<()>, worker_name: &str) -> Option<String> {
match crate::thread_join::join_with_timeout(worker, WORKER_EXIT_JOIN_TIMEOUT) {
Ok(Ok(())) => None,
Ok(Err(_)) => Some(format!("{worker_name} panicked")),
Err(_detached) => Some(format!(
"{worker_name} did not stop within {}ms; detached",
WORKER_EXIT_JOIN_TIMEOUT.as_millis()
)),
}
}
pub(super) fn join_provider_worker(worker: WorkerState) -> Option<String> {
match worker.shutdown_policy {
WorkerShutdownPolicy::Cancel => {
join_critical_or_provider_worker(worker.handle, "provider worker")
}
WorkerShutdownPolicy::WaitForCompletion => worker
.handle
.join()
.err()
.map(|_| "provider worker panicked".to_string()),
}
}
pub(super) fn initial_prompt_text_and_cursor(prompt: Option<&str>) -> (String, usize) {
let text = prompt.unwrap_or_default().to_string();
let cursor = text.len();
(text, cursor)
}
pub(super) fn draw_initial_frame<B>(
terminal: &mut ratatui::Terminal<B>,
state: &mut state::MissionControlState,
) -> Result<ratatui::layout::Rect>
where
B: ratatui::backend::Backend,
B::Error: Send + Sync + 'static,
{
let mut area = ratatui::layout::Rect::default();
terminal.draw(|frame| {
area = frame.area();
render::draw(frame, state);
})?;
Ok(area)
}
use crate::runtime_context::{bounded_startup_diagnostic, enabled_subagent_diagnostic};
fn startup_failure_message(message: impl std::fmt::Display) -> String {
format!(
"Mission Control startup failed: {}",
bounded_startup_diagnostic(message)
)
}
fn load_startup_critical_data(
settings: crate::config::Settings,
discovered_skills: SkillDiscovery,
paths: crate::config::McPaths,
cancellation: &crate::cancellation::AgentCancellation,
) -> anyhow::Result<TuiStartupCritical> {
let policy = crate::runtime_context::StartupPolicy::load(
&settings,
&discovered_skills,
&paths,
cancellation,
)?;
Ok(TuiStartupCritical {
update_restart_history: None,
disabled_tools: policy.disabled_tools,
disabled_subagent_profiles: policy.disabled_subagent_profiles,
skills: policy.skills,
subagent_profile_discovery: policy.subagent_profile_discovery,
primary_agent_discovery: policy.primary_agent_discovery,
selected_primary_agent: policy.selected_primary_agent,
subagent_card_rows: settings.tui.subagent_card_rows(),
mcp: policy.mcp,
})
}
fn load_startup_decorative_data(
config: EffectiveConfig,
manager: SessionManager,
session_snapshot: (Option<String>, u64),
cwd: PathBuf,
skills: SkillDiscovery,
model: String,
provider: String,
) -> TuiStartupDecorative {
let (active_session_id, session_generation) = session_snapshot;
let mut warning = None;
let active_latest_title = active_session_id.as_deref().and_then(|active_id| {
match manager.list_metadata_summaries() {
Ok(summaries) => summaries
.into_iter()
.find(|summary| summary.session.id() == active_id)
.and_then(|summary| summary.latest_title),
Err(error) => {
warning = Some(bounded_startup_diagnostic(format!(
"could not load active session title metadata: {error}"
)));
None
}
}
});
let file_autocomplete_candidates =
tui_file_autocomplete_candidates(&cwd, config.file_autocomplete_respects_gitignore);
let skill_autocomplete_candidates = tui_skill_autocomplete_candidates(&skills);
let thinking_levels = cached_thinking_levels_for_model(
&config.paths,
&provider,
&model,
crate::thinking::capability_scope_for_provider(&config.custom_providers, &provider),
);
let context_max_tokens = crate::config::load_context_budget(&config)
.map(|budget| budget.max_tokens)
.unwrap_or_else(|_| crate::context::ContextBudget::default().max_tokens);
let fast_mode_service_tier = (provider == crate::providers::OPENAI_CODEX_PROVIDER)
.then(|| crate::model_catalog::cached_codex_fast_service_tier(&config.paths, &model))
.flatten();
let footer_git_branch = crate::tui::sessions::commands::current_git_branch(&cwd);
TuiStartupDecorative {
prompt_history: if crate::tui::prompt_history::enabled() {
crate::tui::prompt_history::store_path(&config.paths.state, &cwd)
.and_then(|path| crate::tui::prompt_history::PromptHistory::load(&path))
.unwrap_or_default()
} else {
Default::default()
},
active_session_id,
session_generation,
active_latest_title,
file_autocomplete_candidates,
skill_autocomplete_candidates,
thinking_levels,
context_max_tokens,
footer_git_branch,
provider,
model,
fast_mode_service_tier,
warning,
}
}
impl MissionControlApp {
pub(super) fn start_startup_loader(&mut self) -> u64 {
self.next_startup_request_id = self.next_startup_request_id.saturating_add(1);
let request_id = self.next_startup_request_id;
self.startup_cancel.store(false, Ordering::SeqCst);
self.startup_critical_event_sent
.store(false, Ordering::SeqCst);
let critical_sender = self.events.clone();
let critical_settings = self.settings.clone();
let critical_skills = self.discovered_skills.clone();
let critical_paths = self.config.paths.clone();
let critical_cancel = Arc::clone(&self.startup_cancel);
let critical_event_sent = Arc::clone(&self.startup_critical_event_sent);
let restart_session = self.state.current_session.clone();
let restart_config = self.config.clone();
self.startup_critical_worker = Some(thread::spawn(move || {
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
let cancellation = crate::cancellation::AgentCancellation::new(critical_cancel);
let mut loaded = load_startup_critical_data(
critical_settings,
critical_skills,
critical_paths,
&cancellation,
)?;
loaded.update_restart_history = restart_session
.map(|session| {
crate::tui::sessions::commands::load_admitted_session_switch(
session,
Some(&restart_config),
None,
)
})
.transpose()?;
Ok::<_, anyhow::Error>(loaded)
}))
.map_err(|_| "critical startup worker panicked".to_string())
.and_then(|result| result.map_err(bounded_startup_diagnostic));
let sent = send_tui_event(
&critical_sender,
TuiEvent::StartupCriticalLoaded {
request_id,
result: Box::new(result),
},
)
.is_ok();
critical_event_sent.store(sent, Ordering::SeqCst);
}));
let decorative_sender = self.events.clone();
let decorative_config = self.config.clone();
let decorative_manager = self.state.session_manager.clone();
let decorative_active_session_id = self.state.active_session_id().map(ToOwned::to_owned);
let decorative_session_generation = self.session_generation;
let decorative_cwd = self.state.cwd.clone();
let decorative_skills = self.skills.clone();
let decorative_model = self.state.model.clone();
let decorative_provider = self.state.auth_state.provider().to_string();
let decorative_cancel = Arc::clone(&self.startup_cancel);
self.startup_decorative_worker = Some(thread::spawn(move || {
if decorative_cancel.load(Ordering::SeqCst) {
return;
}
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
load_startup_decorative_data(
decorative_config,
decorative_manager,
(decorative_active_session_id, decorative_session_generation),
decorative_cwd,
decorative_skills,
decorative_model,
decorative_provider,
)
}))
.map_err(|_| "decorative startup worker panicked".to_string());
if decorative_cancel.load(Ordering::SeqCst) {
return;
}
let _ = send_tui_event(
&decorative_sender,
TuiEvent::StartupDecorativeLoaded {
request_id,
result: Box::new(result),
},
);
}));
request_id
}
pub(crate) fn cleanup_after_run(&mut self) -> anyhow::Result<()> {
self.side.request_shutdown();
self.startup_cancel.store(true, Ordering::SeqCst);
self.fast_mode_persistence.request_shutdown();
if let Some(worker) = self.worker.as_ref()
&& worker.shutdown_policy == WorkerShutdownPolicy::Cancel
{
worker.cancel.store(true, Ordering::SeqCst);
worker.steering.clear();
}
self.cancel_rewind_workers();
self.cancel_model_selection_workers();
self.cancel_export_workers();
self.summarizer.shutdown();
let theme_worker_errors = self.join_theme_workers_for_cleanup();
let rewind_worker_errors = self.join_rewind_workers_for_cleanup();
let export_worker_errors = self.join_export_workers_for_cleanup();
let model_selection_worker_errors = self.join_model_selection_workers_for_cleanup();
let preference_save_errors = self.join_preference_save_for_cleanup();
let critical_worker = self.startup_critical_worker.take();
let decorative_worker = self.startup_decorative_worker.take();
let provider_worker = self.worker.take();
let mut errors = theme_worker_errors;
errors.extend(preference_save_errors);
errors.extend(self.release_notes.cleanup());
errors.extend(self.join_auth_worker_for_cleanup());
errors.extend(rewind_worker_errors);
errors.extend(model_selection_worker_errors);
errors.extend(export_worker_errors);
errors.extend(self.fast_mode_persistence.cleanup());
errors.extend(self.settings_persistence.cleanup());
errors.extend(self.panel_layout_persistence.cleanup(&self.config.paths));
errors.extend(self.session_maintenance.cleanup());
errors.extend(self.system_prompt.cleanup());
errors.extend(self.side.cleanup());
errors.extend(self.codex_quota.cleanup());
errors.extend(self.claude_quota.cleanup());
if let Some(worker) = self.footer_git_branch_worker.take() {
match worker.join_with_timeout(WORKER_EXIT_JOIN_TIMEOUT) {
Some(Ok(_)) => {}
Some(Err(error)) => errors.push(format!("footer branch worker: {error}")),
None => errors.push("footer branch worker did not finish before shutdown".into()),
}
}
self.pending_footer_git_branch_request_id = None;
if let Some(worker) = critical_worker
&& let Some(error) = join_critical_or_provider_worker(worker, "critical startup worker")
{
errors.push(error);
}
if let Some(worker) = decorative_worker
&& join_decorative_startup_worker(worker)
{
errors.push("decorative startup worker panicked".to_string());
}
if let Some(worker) = provider_worker
&& let Some(error) = join_provider_worker(worker)
{
errors.push(error);
}
self.active_run = false;
if errors.is_empty() {
Ok(())
} else {
Err(anyhow::anyhow!(errors.join("; ")))
}
}
fn status_with_initial_prompt_rejection(&self, status: String) -> String {
if self.initial_prompt_rejected {
if status == "Mission Control ready" {
state::PROMPT_TOO_LARGE_STATUS.to_string()
} else {
format!("{status}; {}", state::PROMPT_TOO_LARGE_STATUS)
}
} else {
status
}
}
pub(super) fn startup_initial_status(&self) -> String {
if self.initial_prompt_rejected {
state::PROMPT_TOO_LARGE_STATUS.to_string()
} else {
"starting Mission Control…".to_string()
}
}
fn set_ready_status(&self, ui_state: &mut state::MissionControlState) {
let status = match &self.startup_warning {
Some(warning) => format!("Mission Control ready; startup warning: {warning}"),
None => "Mission Control ready".to_string(),
};
ui_state.status = self.status_with_initial_prompt_rejection(status);
}
fn fail_startup(
&mut self,
ui_state: &mut state::MissionControlState,
error: impl std::fmt::Display,
) {
if self.startup_readiness == StartupReadiness::Failed {
return;
}
let message = startup_failure_message(error);
self.startup_readiness = StartupReadiness::Failed;
self.startup_failure = Some(message.clone());
self.clear_pending_startup_prompt();
ui_state.clear_prompt_input();
ui_state.status = self.status_with_initial_prompt_rejection(message);
ui_state.startup_prompt_queued = false;
}
pub(super) fn check_startup_worker_health(
&mut self,
ui_state: &mut state::MissionControlState,
) {
let finished = self
.startup_critical_worker
.as_ref()
.is_some_and(|worker| worker.is_finished());
if finished
&& self.startup_readiness == StartupReadiness::Loading
&& !self.startup_critical_event_sent.load(Ordering::SeqCst)
{
self.fail_startup(
ui_state,
"critical startup worker exited without publishing a readiness result",
);
}
}
pub(super) fn apply_startup_critical(
&mut self,
ui_state: &mut state::MissionControlState,
loaded: TuiStartupCritical,
) -> anyhow::Result<()> {
if let Some(history) = loaded.update_restart_history
&& self.state.active_session_id() == Some(history.session.id())
{
apply_session_hydration_snapshot(ui_state, history.snapshot);
self.restore_session_preferences(ui_state, history.preferences);
apply_footer_context(
ui_state,
self.state.current_session.as_ref(),
history.latest_title.as_deref(),
&self.state.cwd,
);
for message in history.diagnostics {
ui_state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".to_string(),
message,
});
}
}
{
let mut disabled_tools = self
.disabled_tools
.lock()
.map_err(|_| anyhow::anyhow!("disabled tools lock poisoned"))?;
*disabled_tools = loaded.disabled_tools.clone();
}
{
let mut disabled_subagents = self
.disabled_subagent_profiles
.lock()
.map_err(|_| anyhow::anyhow!("disabled subagent profiles lock poisoned"))?;
*disabled_subagents = loaded.disabled_subagent_profiles.clone();
}
let subagent_warnings = loaded
.subagent_profile_discovery
.diagnostics
.iter()
.filter(|diagnostic| {
!enabled_subagent_diagnostic(diagnostic, &loaded.disabled_subagent_profiles)
})
.take(5)
.map(|diagnostic| {
format!(
"subagent profile skipped: profile={} reason={}",
bounded_startup_diagnostic(
diagnostic
.id
.as_deref()
.or_else(|| diagnostic.path.as_deref().and_then(|path| path.to_str()))
.unwrap_or("<unknown>")
),
bounded_startup_diagnostic(&diagnostic.message)
)
})
.collect::<Vec<_>>();
self.skills = loaded.skills;
self.subagent_profile_discovery = loaded.subagent_profile_discovery;
self.mcp = loaded.mcp;
ui_state.subagent_card_rows = loaded.subagent_card_rows;
ui_state.invalidate_transcript_cache();
ui_state.set_primary_agents(
loaded
.primary_agent_discovery
.profiles
.into_values()
.map(Into::into)
.collect(),
);
ui_state.set_selected_primary_agent_id(loaded.selected_primary_agent.as_deref());
record_primary_agent_startup_diagnostics(
ui_state,
&loaded.primary_agent_discovery.diagnostics,
);
for warning in subagent_warnings {
ui_state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".to_string(),
message: warning,
});
}
self.startup_readiness = StartupReadiness::Ready;
self.set_ready_status(ui_state);
Ok(())
}
fn apply_startup_decorative(
&mut self,
ui_state: &mut state::MissionControlState,
loaded: TuiStartupDecorative,
autocomplete_candidates: &mut Vec<state::AutocompleteCandidate>,
) {
if !ui_state.prompt_history_loaded {
ui_state.prompt_history = loaded.prompt_history;
ui_state.prompt_history_loaded = true;
}
let startup_selection_is_current =
loaded.provider == ui_state.provider && loaded.model == ui_state.model;
let newer_catalog_has_current_selection =
self.model_catalog_cache.as_ref().is_some_and(|catalog| {
catalog.entries.iter().any(|entry| {
entry.provider == ui_state.provider && entry.model == ui_state.model
})
});
autocomplete_candidates.extend(loaded.file_autocomplete_candidates);
autocomplete_candidates.extend(loaded.skill_autocomplete_candidates);
if startup_selection_is_current && !newer_catalog_has_current_selection {
ui_state.refresh_thinking_levels(self.config.thinking_level, loaded.thinking_levels);
}
if startup_selection_is_current {
ui_state.update_context_window(loaded.context_max_tokens);
}
let active_session_id = self.state.active_session_id();
let startup_title_is_current = loaded.session_generation == self.session_generation
&& loaded.active_session_id.as_deref() == active_session_id
&& ui_state.footer_session_id.as_deref() == active_session_id
&& match active_session_id {
Some(session_id) => {
let short_id = crate::tui::sessions::commands::short_session_id(session_id);
ui_state.footer_session == short_id || ui_state.footer_session == session_id
}
None => ui_state.footer_session == "<disabled>",
};
if startup_title_is_current {
apply_footer_context(
ui_state,
self.state.current_session.as_ref(),
loaded.active_latest_title.as_deref(),
&self.state.cwd,
);
} else {
ui_state.footer_cwd = self.state.cwd.display().to_string();
}
ui_state.footer_git_branch = loaded.footer_git_branch;
if startup_selection_is_current {
self.refresh_fast_mode_state(ui_state);
}
if let Some(warning) = loaded.warning {
self.startup_warning = Some(bounded_startup_diagnostic(warning));
}
if self.startup_readiness == StartupReadiness::Ready {
self.set_ready_status(ui_state);
} else if let Some(warning) = &self.startup_warning {
ui_state.status = self.status_with_initial_prompt_rejection(format!(
"startup details warning: {warning}"
));
}
}
pub(super) fn handle_startup_drain(
&mut self,
ui_state: &mut state::MissionControlState,
drain_result: &mut DrainResult,
autocomplete_candidates: &mut Vec<state::AutocompleteCandidate>,
) -> bool {
let mut changed = false;
for (_request_id, loaded) in std::mem::take(&mut drain_result.startup_critical_loaded) {
match loaded {
Ok(loaded) => {
if let Err(error) = self.apply_startup_critical(ui_state, loaded) {
self.fail_startup(ui_state, error);
}
changed = true;
}
Err(error) => {
self.fail_startup(ui_state, error);
changed = true;
}
}
}
if self.startup_readiness == StartupReadiness::Failed {
return changed;
}
for (_request_id, loaded) in std::mem::take(&mut drain_result.startup_decorative_loaded) {
match loaded {
Ok(loaded) => {
self.apply_startup_decorative(ui_state, loaded, autocomplete_candidates)
}
Err(error) => {
self.startup_warning = Some(bounded_startup_diagnostic(error));
if self.startup_readiness == StartupReadiness::Ready {
self.set_ready_status(ui_state);
} else {
ui_state.status = self.status_with_initial_prompt_rejection(format!(
"startup details warning: {}",
self.startup_warning.as_deref().unwrap_or("unavailable")
));
}
}
}
changed = true;
}
changed
}
pub(super) fn initialize_startup_prompt_queue(&mut self, painted: bool) {
if self.pending_startup_prompt.is_none()
&& let Some(prompt) = self.pending_initial_prompt.clone()
{
self.pending_startup_prompt = Some(PendingStartupPrompt {
prompt,
painted,
initial_candidate: true,
});
}
}
pub(super) fn mark_pending_startup_prompt_painted(&mut self) {
if let Some(pending) = self.pending_startup_prompt.as_mut() {
pending.painted = true;
}
}
pub(super) fn clear_pending_startup_prompt(&mut self) {
self.pending_startup_prompt = None;
self.pending_initial_prompt = None;
self.initial_prompt_flush_ack = None;
}
pub(super) fn cancel_initial_startup_prompt(&mut self) {
let initial_candidate = self
.pending_startup_prompt
.as_ref()
.is_some_and(|pending| pending.initial_candidate);
if initial_candidate
|| (self.pending_startup_prompt.is_none() && self.pending_initial_prompt.is_some())
{
self.pending_startup_prompt = None;
self.pending_initial_prompt = None;
self.initial_prompt_flush_ack = None;
}
}
pub(super) fn sync_startup_prompt_signal(&self, ui_state: &mut state::MissionControlState) {
ui_state.startup_prompt_queued = self.pending_startup_prompt.is_some();
}
pub(super) fn mark_initial_prompt_input(
&mut self,
input_changed: bool,
action: Option<&input::InputAction>,
ui_state: &mut state::MissionControlState,
) {
let normal_prompt_submit = action.is_some_and(|action| {
let input::InputAction::Submit(prompt) = action else {
return false;
};
!prompt.trim().is_empty()
&& matches!(
input::submit_command::tui_submit_command(prompt, &self.commands),
input::submit_command::TuiSubmitCommand::None
)
});
let action_requires_new_flush = action.is_some_and(|action| {
matches!(
action,
input::InputAction::Submit(_)
| input::InputAction::CancelStartupPrompt
| input::InputAction::Exit
)
});
if input_changed || action_requires_new_flush {
self.initial_prompt_flush_ack = None;
}
if matches!(action, Some(input::InputAction::Exit)) {
self.clear_pending_startup_prompt();
self.sync_startup_prompt_signal(ui_state);
return;
}
let initial_candidate = self
.pending_startup_prompt
.as_ref()
.is_some_and(|pending| pending.initial_candidate)
|| (self.pending_startup_prompt.is_none() && self.pending_initial_prompt.is_some());
if initial_candidate
&& ((input_changed && !normal_prompt_submit)
|| action.is_some_and(|action| {
matches!(action, input::InputAction::Submit(_)) && !normal_prompt_submit
}))
{
self.cancel_initial_startup_prompt();
}
self.sync_startup_prompt_signal(ui_state);
}
pub(super) fn request_initial_prompt_flush(
&mut self,
input_events: &TerminalInputBridge,
) -> anyhow::Result<()> {
if self.pending_export.is_some() {
return Ok(());
}
if self.startup_readiness == StartupReadiness::Failed {
self.clear_pending_startup_prompt();
return Ok(());
}
self.initialize_startup_prompt_queue(true);
if self.startup_readiness == StartupReadiness::Ready
&& self
.pending_startup_prompt
.as_ref()
.is_some_and(|pending| pending.painted)
&& self.initial_prompt_flush_ack.is_none()
{
if input_events.reader_finished() {
return Err(anyhow::anyhow!(
"terminal input reader disconnected before startup prompt input fence"
));
}
self.initial_prompt_flush_ack = input_events.flush();
if self.initial_prompt_flush_ack.is_none() && input_events.reader_finished() {
return Err(anyhow::anyhow!(
"terminal input reader disconnected before startup prompt input fence"
));
}
}
Ok(())
}
fn submit_queued_startup_prompt(
&mut self,
prompt: String,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
terminal_area: ratatui::layout::Rect,
) -> bool {
self.startup_prompt_launch_in_progress = true;
let accepted = self.submit(prompt, ui_state, receiver, terminal_area);
self.startup_prompt_launch_in_progress = false;
accepted
}
pub(super) fn submit_initial_prompt_if_ready(
&mut self,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
terminal_area: ratatui::layout::Rect,
input_receiver: &Receiver<TerminalInputEvent>,
pending_input_events: &mut VecDeque<TerminalInputEvent>,
) -> anyhow::Result<Option<actions::AppControlFlow>> {
if self.startup_readiness == StartupReadiness::Failed {
self.clear_pending_startup_prompt();
self.sync_startup_prompt_signal(ui_state);
return Ok(None);
}
self.initialize_startup_prompt_queue(true);
self.sync_startup_prompt_signal(ui_state);
let Some(pending) = self.pending_startup_prompt.clone() else {
return Ok(None);
};
if self.startup_readiness == StartupReadiness::Loading {
if !pending.initial_candidate {
return Ok(None);
}
if !pending_input_events.is_empty() || !input_receiver.is_empty() {
return Ok(None);
}
if ui_state.prompt_content_revision() != 0 {
self.cancel_initial_startup_prompt();
self.sync_startup_prompt_signal(ui_state);
return Ok(None);
}
let command =
input::submit_command::tui_submit_command(&pending.prompt, &self.commands);
match command {
input::submit_command::TuiSubmitCommand::Quit => {
self.clear_pending_startup_prompt();
ui_state.clear_prompt_input();
self.sync_startup_prompt_signal(ui_state);
return Ok(Some(actions::AppControlFlow::Exit));
}
input::submit_command::TuiSubmitCommand::Help => {
self.clear_pending_startup_prompt();
ui_state.clear_prompt_input();
self.sync_startup_prompt_signal(ui_state);
let _ = self.submit(pending.prompt, ui_state, receiver, terminal_area);
}
input::submit_command::TuiSubmitCommand::None
if pending.prompt.trim().is_empty() =>
{
self.clear_pending_startup_prompt();
ui_state.clear_prompt_input();
self.sync_startup_prompt_signal(ui_state);
}
_ => {
let _ = self.submit(pending.prompt, ui_state, receiver, terminal_area);
}
}
self.sync_startup_prompt_signal(ui_state);
return Ok(Some(actions::AppControlFlow::Continue));
}
if !pending.painted {
return Ok(None);
}
if self.active_run
|| self.worker.is_some()
|| self.pending_session_switch.is_some()
|| self.pending_export.is_some()
{
return Ok(None);
}
let Some(flush_ack) = self.initial_prompt_flush_ack.as_ref() else {
return Ok(None);
};
match flush_ack.try_recv() {
Ok(()) => self.initial_prompt_flush_ack = None,
Err(crossbeam_channel::TryRecvError::Empty) => return Ok(None),
Err(crossbeam_channel::TryRecvError::Disconnected) => {
return Err(anyhow::anyhow!(
"terminal input reader disconnected while waiting for startup prompt input fence"
));
}
}
match input_receiver.try_recv() {
Ok(event) => pending_input_events.push_back(event),
Err(crossbeam_channel::TryRecvError::Empty) => {}
Err(crossbeam_channel::TryRecvError::Disconnected) => {
return Err(anyhow::anyhow!(
"terminal input reader disconnected while waiting for startup prompt input fence"
));
}
}
if !pending_input_events.is_empty() {
return Ok(None);
}
if pending.initial_candidate && ui_state.prompt_content_revision() != 0 {
self.cancel_initial_startup_prompt();
self.sync_startup_prompt_signal(ui_state);
return Ok(None);
}
let command = input::submit_command::tui_submit_command(&pending.prompt, &self.commands);
if matches!(command, input::submit_command::TuiSubmitCommand::Quit) {
self.clear_pending_startup_prompt();
if pending.initial_candidate {
ui_state.clear_prompt_input();
}
self.sync_startup_prompt_signal(ui_state);
return Ok(Some(actions::AppControlFlow::Exit));
}
let normal_prompt = !pending.prompt.trim().is_empty()
&& matches!(command, input::submit_command::TuiSubmitCommand::None);
if !normal_prompt {
self.clear_pending_startup_prompt();
if pending.initial_candidate {
ui_state.clear_prompt_input();
}
self.sync_startup_prompt_signal(ui_state);
let _ = self.submit(pending.prompt, ui_state, receiver, terminal_area);
return Ok(Some(actions::AppControlFlow::Continue));
}
let accepted = self.submit_queued_startup_prompt(
pending.prompt.clone(),
ui_state,
receiver,
terminal_area,
);
if accepted {
self.clear_pending_startup_prompt();
if pending.initial_candidate {
ui_state.clear_prompt_input();
}
self.sync_startup_prompt_signal(ui_state);
Ok(Some(actions::AppControlFlow::Continue))
} else {
self.sync_startup_prompt_signal(ui_state);
Ok(None)
}
}
}