#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ModelSelectionFinished {
pub(crate) request_id: u64,
pub(crate) session_id: Option<String>,
pub(crate) generation: u64,
pub(crate) result: Result<crate::tui::TuiModelSelectionResult, String>,
}
use super::*;
pub(crate) const UI_DRAIN_BUDGET: DrainBudget = DrainBudget {
max_events: 128,
max_elapsed: Duration::from_millis(2),
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DrainBudget {
pub(crate) max_events: usize,
pub(crate) max_elapsed: Duration,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct DrainOutcome {
pub(crate) result: DrainResult,
pub(crate) processed: usize,
pub(crate) exhausted: bool,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct DrainResult {
pub(crate) changed: bool,
pub(crate) run_finished: bool,
pub(crate) run_finished_worker_ids: Vec<u64>,
pub(crate) model_catalog_finished: bool,
pub(crate) model_catalog_loaded: Vec<(
u64,
Result<crate::model_catalog::CatalogForUi, String>,
usize,
)>,
pub(crate) fast_mode_persisted: Vec<(u64, bool, Result<(), String>)>,
pub(crate) theme_catalog_loaded: Vec<(u64, Result<crate::appearance::ThemeCatalog, String>)>,
pub(crate) theme_persisted: Vec<(u64, String, u64, Result<(), String>)>,
pub(crate) startup_critical_loaded: Vec<(u64, Result<TuiStartupCritical, String>)>,
pub(crate) usage_loaded: Vec<(u64, crate::tui::usage::UsageLoadResult)>,
pub(crate) startup_decorative_loaded: Vec<(u64, Result<TuiStartupDecorative, String>)>,
pub(crate) custom_provider_finished: Vec<(String, Result<String, String>)>,
pub(crate) session_preview_loaded: Vec<(u64, String, state::SessionPreviewState)>,
pub(crate) session_switch_loaded: Vec<(u64, String, Result<SessionSwitchLoad, String>)>,
pub(crate) session_title_updated: Vec<(String, String)>,
pub(crate) rewind_finished: Vec<(
u64,
String,
u64,
Result<crate::tui::TuiRewindWorkerResult, String>,
)>,
pub(crate) export_finished: Vec<(
u64,
String,
u64,
Result<crate::tui::TuiExportWorkerResult, String>,
)>,
pub(crate) model_selection_finished: Vec<ModelSelectionFinished>,
pub(crate) compaction_runtime_refresh: Vec<worker::CompactionRuntimeRefresh>,
pub(crate) footer_git_branch_loaded: Vec<(u64, Option<String>)>,
}
#[cfg(test)]
mod profiling_support;
#[cfg(test)]
pub(crate) use profiling_support::*;
pub(crate) fn drain_tui_events_with_pending(
receiver: &Receiver<TuiEvent>,
pending: &mut VecDeque<TuiEvent>,
state: &mut state::MissionControlState,
budget: DrainBudget,
) -> DrainOutcome {
let started = Instant::now();
let mut outcome = DrainOutcome::default();
while !pending.is_empty() {
if outcome.processed >= budget.max_events
|| (outcome.processed > 0 && started.elapsed() >= budget.max_elapsed)
{
outcome.exhausted = true;
return outcome;
}
let event = pending.pop_front().unwrap();
apply_tui_event_to_state(state, event, &mut outcome.result);
outcome.processed += 1;
}
let tail = drain_tui_events_budgeted_from(
receiver,
state,
budget,
outcome.processed,
started,
Instant::now,
);
outcome.processed = tail.processed;
outcome.exhausted = tail.exhausted;
outcome.result.merge(tail.result);
outcome
}
pub(crate) fn drain_tui_events_budgeted_from(
receiver: &Receiver<TuiEvent>,
state: &mut state::MissionControlState,
budget: DrainBudget,
already_processed: usize,
started: Instant,
mut now: impl FnMut() -> Instant,
) -> DrainOutcome {
let mut outcome = DrainOutcome {
processed: already_processed,
..Default::default()
};
if outcome.processed >= budget.max_events {
outcome.exhausted = true;
return outcome;
}
while outcome.processed < budget.max_events {
if outcome.processed > 0 && now().duration_since(started) >= budget.max_elapsed {
outcome.exhausted = true;
return outcome;
}
let Ok(event) = receiver.try_recv() else {
return outcome;
};
apply_tui_event_to_state(state, event, &mut outcome.result);
outcome.processed += 1;
}
outcome.exhausted = true;
outcome
}
pub(crate) fn apply_tui_event_to_state(
state: &mut state::MissionControlState,
event: TuiEvent,
result: &mut DrainResult,
) {
match event {
TuiEvent::Output(event) => apply_output_event_to_state(state, event, result),
TuiEvent::Activity(event) => apply_activity_event_to_state(state, event, result),
event => apply_control_event_to_state(state, event, result),
}
}
fn apply_output_event_to_state(
state: &mut state::MissionControlState,
event: OutputEvent,
result: &mut DrainResult,
) {
if matches!(
event,
OutputEvent::UserPrompt { .. }
| OutputEvent::SteeringInjected { .. }
| OutputEvent::AutomaticUserPrompt { .. }
| OutputEvent::AssistantDelta { .. }
| OutputEvent::AssistantComplete { .. }
| OutputEvent::ThinkingSummaryDelta { .. }
| OutputEvent::ThinkingSummaryComplete { .. }
| OutputEvent::ThinkingSummaryCompleteIdentified { .. }
| OutputEvent::ToolStarted { .. }
| OutputEvent::ToolResult { .. }
| OutputEvent::UsageSnapshot { .. }
) {
state.last_agent_activity = Some(Instant::now());
}
state.observe_live_tool_result_completion(&event, Instant::now());
state.apply_output_event(&event);
result.changed = true;
}
pub(crate) fn apply_activity_event_to_state(
state: &mut state::MissionControlState,
event: ActivityEvent,
result: &mut DrainResult,
) {
state.last_agent_activity = Some(Instant::now());
state.observe_live_card_completion(&event, std::time::Instant::now());
state.apply_activity_event(event);
result.changed = true;
}
fn apply_worker_final_event_to_state(
state: &mut state::MissionControlState,
event: TuiEvent,
result: &mut DrainResult,
) {
let Some(event) = WorkerFinalEvent::from_tui_event(event) else {
return;
};
match apply_worker_final_event(state, event) {
WorkerFinalEventEffect::CustomProviderFinished {
provider_id,
result: outcome,
} => result.custom_provider_finished.push((provider_id, outcome)),
WorkerFinalEventEffect::CompactionRuntimeRefresh(refresh) => {
result.compaction_runtime_refresh.push(*refresh);
}
WorkerFinalEventEffect::None => {}
}
}
pub(crate) fn apply_control_event_to_state(
state: &mut state::MissionControlState,
event: TuiEvent,
result: &mut DrainResult,
) {
match event {
TuiEvent::WorkerOutcomeReady => {}
TuiEvent::BashApprovalRequested { request, response } => {
state.open_bash_approval(request, response);
state.status = "bash command waiting for approval".to_string();
}
TuiEvent::Error(error) => {
apply_worker_final_event_to_state(state, TuiEvent::Error(error), result);
}
TuiEvent::WorkerCompletionDeliveryFailed { worker_id, error } => {
if state.active_worker_id != Some(worker_id) {
return;
}
state.status = format!("worker completion delivery failed: {error}");
}
TuiEvent::PromptHistoryLoaded(history) => {
state.prompt_history = history;
state.prompt_history_loaded = true;
state.visible_prompt_history_revision.set(None);
}
TuiEvent::ModelCatalog {
request_id,
result: catalog_result,
visible_rows,
} => {
if state.pending_model_catalog_request_id != Some(request_id) {
return;
}
state.pending_model_catalog_request_id = None;
result.model_catalog_finished = true;
result
.model_catalog_loaded
.push((request_id, catalog_result, visible_rows));
}
TuiEvent::FastModePersisted {
request_id,
enabled,
result: persistence_result,
} => {
result
.fast_mode_persisted
.push((request_id, enabled, persistence_result));
}
TuiEvent::ModelSelectionFinished {
request_id,
session_id,
generation,
result: selection_result,
} => {
result
.model_selection_finished
.push(ModelSelectionFinished {
request_id,
session_id,
generation,
result: *selection_result,
});
}
TuiEvent::UsageLoaded {
request_id,
result: usage_result,
} => {
if state.pending_usage_request_id != Some(request_id) || !state.usage_modal_visible() {
return;
}
state.pending_usage_request_id = None;
let status = match &usage_result {
crate::tui::usage::UsageLoadResult::Loaded(report)
if report.providers.is_empty() =>
{
"no provider usage data available"
}
crate::tui::usage::UsageLoadResult::Loaded(_) => "provider usage loaded",
crate::tui::usage::UsageLoadResult::Error(_) => "failed to load provider usage",
};
state.set_usage_modal_result(usage_result.clone());
state.status = status.to_string();
result.usage_loaded.push((request_id, usage_result));
}
TuiEvent::OAuthInstructions {
worker_id,
instructions,
} => {
if state.set_connect_provider_instructions_for(
worker_id,
instructions.url,
instructions.message,
) {
state.status = "OpenAI Codex login waiting for browser callback or pasted redirect"
.to_string();
}
}
TuiEvent::CompactionFinishedAuthoritative {
result: outcome,
activity,
runtime_config,
} => {
apply_worker_final_event_to_state(
state,
TuiEvent::CompactionFinishedAuthoritative {
result: outcome,
activity,
runtime_config,
},
result,
);
}
TuiEvent::OAuthFinished {
worker_id,
provider_id,
result: outcome,
} => {
if state.active_worker_id != Some(worker_id) {
return;
}
apply_worker_final_event_to_state(
state,
TuiEvent::OAuthFinished {
worker_id,
provider_id,
result: outcome,
},
result,
);
}
TuiEvent::CustomProviderFinished {
worker_id,
provider_id,
result: outcome,
} => {
if state.active_worker_id != Some(worker_id) {
return;
}
apply_worker_final_event_to_state(
state,
TuiEvent::CustomProviderFinished {
worker_id,
provider_id,
result: outcome,
},
result,
);
}
TuiEvent::Done => {
apply_worker_final_event_to_state(state, TuiEvent::Done, result);
}
TuiEvent::RunCanceled { prompt } => {
apply_worker_final_event_to_state(state, TuiEvent::RunCanceled { prompt }, result);
}
TuiEvent::RunFinished {
worker_id,
final_event,
} => {
if state.active_worker_id != Some(worker_id) {
return;
}
if let Some(event) = final_event {
apply_worker_final_event_to_state(state, *event, result);
}
if state.modals.bash_approval.is_some() {
state.resolve_bash_approval(false);
}
state.finish_assistant_streaming();
state.clear_running_prompt();
state.active_worker_id = None;
state.last_run_finished_worker_id = Some(worker_id);
result.run_finished = true;
result.run_finished_worker_ids.push(worker_id);
}
TuiEvent::SessionTitleUpdated { session_id, title } => {
state.apply_session_title_update(&session_id, &title);
result.session_title_updated.push((session_id, title));
}
TuiEvent::StartupCriticalLoaded {
request_id,
result: loaded,
} => {
if state.pending_startup_critical_request_id != Some(request_id) {
return;
}
state.pending_startup_critical_request_id = None;
match *loaded {
Ok(loaded) => result
.startup_critical_loaded
.push((request_id, Ok(loaded))),
Err(error) => result
.startup_critical_loaded
.push((request_id, Err(error))),
}
}
TuiEvent::StartupDecorativeLoaded {
request_id,
result: loaded,
} => {
if state.pending_startup_decorative_request_id != Some(request_id) {
return;
}
state.pending_startup_decorative_request_id = None;
match *loaded {
Ok(loaded) => result
.startup_decorative_loaded
.push((request_id, Ok(loaded))),
Err(error) => result
.startup_decorative_loaded
.push((request_id, Err(error))),
}
}
TuiEvent::SessionPreviewLoaded {
request_id,
session_id,
preview,
} => {
result
.session_preview_loaded
.push((request_id, session_id, preview));
}
TuiEvent::SessionSwitchLoaded {
request_id,
session_id,
result: switch_result,
} => {
result
.session_switch_loaded
.push((request_id, session_id, *switch_result));
}
TuiEvent::RewindFinished {
request_id,
session_id,
generation,
result: rewind_result,
} => {
result
.rewind_finished
.push((request_id, session_id, generation, *rewind_result));
}
TuiEvent::ExportFinished {
request_id,
session_id,
generation,
result: export_result,
} => {
result
.export_finished
.push((request_id, session_id, generation, *export_result));
}
TuiEvent::FooterGitBranchLoaded { request_id, branch } => {
result.footer_git_branch_loaded.push((request_id, branch));
}
TuiEvent::ThemeCatalogLoaded {
request_id,
result: catalog_result,
} => {
result
.theme_catalog_loaded
.push((request_id, catalog_result));
}
TuiEvent::ThemePersisted {
request_id,
theme_id,
revision,
result: persistence_result,
} => {
result
.theme_persisted
.push((request_id, theme_id, revision, persistence_result));
}
TuiEvent::Output(_) | TuiEvent::Activity(_) => {}
}
result.changed = true;
}
pub(crate) fn picker_visible_rows(list_area: Option<ratatui::layout::Rect>) -> usize {
list_area
.map(|area| area.height.max(1) as usize)
.unwrap_or(input::DEFAULT_PICKER_VISIBLE_ROWS as usize)
}