#[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)]
pub(crate) fn drain_tui_events(
receiver: &Receiver<TuiEvent>,
state: &mut state::MissionControlState,
) -> DrainResult {
let mut result = DrainResult::default();
while let Ok(event) = receiver.try_recv() {
apply_tui_event_to_state(state, event, &mut result);
}
result
}
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
}
#[cfg(test)]
pub(crate) fn drain_tui_events_budgeted(
receiver: &Receiver<TuiEvent>,
state: &mut state::MissionControlState,
budget: DrainBudget,
) -> DrainOutcome {
drain_tui_events_budgeted_with_clock(receiver, state, budget, Instant::now)
}
#[cfg(test)]
pub(crate) fn drain_tui_events_budgeted_with_clock(
receiver: &Receiver<TuiEvent>,
state: &mut state::MissionControlState,
budget: DrainBudget,
mut now: impl FnMut() -> Instant,
) -> DrainOutcome {
let started = now();
drain_tui_events_budgeted_from(receiver, state, budget, 0, started, now)
}
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,
) {
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.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::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::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::MissingCli => "quota-axi is not installed",
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);
}
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;
}
#[cfg(test)]
pub(crate) fn drain_stale_tui_events(receiver: &Receiver<TuiEvent>) -> DrainResult {
let mut result = DrainResult::default();
while let Ok(event) = receiver.try_recv() {
if let TuiEvent::RunFinished { worker_id, .. } = event {
result.run_finished = true;
result.run_finished_worker_ids.push(worker_id);
}
result.changed = true;
}
result
}
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)
}
#[cfg(test)]
mod motion_tests {
use super::*;
use crate::output::{
ActivityId, ActivityKind, ActivityMetadata, ActivityStatus, ToolActivityDetail,
};
#[test]
fn activity_motion_live_delivery_highlights_each_success_event_shape_once() {
let id = ActivityId::new("tool");
let events = [
ActivityEvent::Finished {
id: id.clone(),
status: ActivityStatus::Success,
metadata: None,
},
ActivityEvent::FinalPreview {
id: id.clone(),
status: Some(ActivityStatus::Success),
metadata: None,
preview: "done".into(),
},
ActivityEvent::ToolResultDetail {
id: id.clone(),
detail: ToolActivityDetail {
tool_name: "write".into(),
label: "write file".into(),
params: serde_json::json!({}),
metadata: serde_json::json!({}),
status: ActivityStatus::Success,
output: "done".into(),
applied_diff: None,
},
},
];
for event in events {
let mut state = state::MissionControlState::default();
state.start_running_prompt("work".into());
let mut result = DrainResult::default();
apply_activity_event_to_state(
&mut state,
ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Writing,
metadata: ActivityMetadata::new("write file"),
},
&mut result,
);
apply_activity_event_to_state(&mut state, event.clone(), &mut result);
assert!(state.card_completion_highlight_visible(&id));
state.tick_activity_motion(Instant::now() + Duration::from_secs(1));
apply_activity_event_to_state(&mut state, event, &mut result);
assert!(!state.card_completion_highlight_visible(&id));
}
}
}