mod activity;
mod autocomplete;
mod events;
mod input;
mod layout;
mod perf;
mod prompt_editor;
pub(crate) mod render;
mod screens;
mod selection;
mod session_usage;
mod sessions;
mod single_line_field;
pub(crate) mod state;
pub(crate) mod terminal;
mod theme;
mod toast;
mod transcript;
mod transcript_cards;
pub(crate) mod transcript_projection;
pub(crate) mod usage;
mod usage_format;
mod viewports;
mod worker;
use self::{
events::delivery::send_tui_event,
input::{
autocomplete_sources::{
refresh_skill_autocomplete_candidates, tui_autocomplete_candidates,
tui_file_autocomplete_candidates, tui_skill_autocomplete_candidates,
},
mouse::mouse_scroll_batch,
submit_command::{SubmitDecision, TuiSubmitCommand, submit_decision, tui_submit_command},
},
screens::primary_agent_startup::{
record_primary_agent_startup_diagnostics, resolve_persisted_primary_agent_selection,
},
sessions::commands::{
SessionSwitchLoad, apply_footer_context, apply_session_hydration_snapshot,
execute_new_session_command, execute_prune_sessions_command, load_session_preview,
preserve_critical_tui_events, session_picker_rows, short_session_id,
start_session_switch_load,
},
worker::{
CompactionActivityFinal, CompactionRuntimeRefresh, WorkerFinalEvent,
WorkerFinalEventEffect, WorkerOutcomeState, WorkerShutdownPolicy, WorkerState,
apply_worker_final_event, send_completion,
},
};
use crate::{
agent::runner::{
ProviderRunOptions, append_primary_agent_to_main_prompt, run_bash_mode_once,
run_provider_once_streaming_with_steering,
},
commands::CommandRegistry,
config::EffectiveConfig,
instructions::InstructionFile,
output::{ActivityEvent, ActivitySender, OutputEvent},
sessions::{Session, SessionEventKind, SessionManager, record_session_event},
shell::ShellState,
skills::{SkillDiscovery, filter_enabled_skills},
};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, bounded};
#[cfg(test)]
use std::fs;
use std::{
collections::VecDeque,
panic,
path::PathBuf,
process::Command,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
mpsc,
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
pub(crate) struct TuiSessionConfig {
pub(crate) config: EffectiveConfig,
pub(crate) settings: crate::config::Settings,
pub(crate) appearance: crate::appearance::RuntimeAppearance,
pub(crate) theme_cli_override: bool,
pub(crate) instructions: Vec<InstructionFile>,
pub(crate) discovered_skills: SkillDiscovery,
pub(crate) skills: SkillDiscovery,
pub(crate) commands: CommandRegistry,
pub(crate) manager: SessionManager,
pub(crate) active_session: Option<Session>,
pub(crate) cwd: PathBuf,
pub(crate) initial_prompt: Option<String>,
pub(crate) herdr_reporter: Option<crate::herdr::HerdrReporter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StartupReadiness {
Loading,
Ready,
Failed,
}
#[derive(Debug, Clone)]
pub(crate) struct TuiStartupCritical {
pub(crate) disabled_tools: std::collections::HashSet<String>,
pub(crate) disabled_subagent_profiles: std::collections::HashSet<String>,
pub(crate) skills: SkillDiscovery,
pub(crate) subagent_profile_discovery: crate::subagents::profiles::SubagentProfileDiscovery,
pub(crate) primary_agent_discovery: crate::primary_agents::PrimaryAgentProfileDiscovery,
pub(crate) selected_primary_agent: Option<String>,
pub(crate) subagent_card_rows: usize,
pub(crate) mcp: Option<std::sync::Arc<std::sync::Mutex<crate::mcp::manager::McpManager>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TuiStartupDecorative {
pub(crate) active_session_id: Option<String>,
pub(crate) session_generation: u64,
pub(crate) active_latest_title: Option<String>,
pub(crate) file_autocomplete_candidates: Vec<state::AutocompleteCandidate>,
pub(crate) skill_autocomplete_candidates: Vec<state::AutocompleteCandidate>,
pub(crate) thinking_levels: Vec<crate::thinking::ThinkingLevel>,
pub(crate) context_max_tokens: usize,
pub(crate) footer_git_branch: Option<String>,
pub(crate) provider: String,
pub(crate) model: String,
pub(crate) fast_mode_service_tier: Option<String>,
pub(crate) warning: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TuiModelSelectionResult {
pub(crate) provider: String,
pub(crate) model: String,
pub(crate) notice: Option<String>,
pub(crate) service_tier: Option<String>,
pub(crate) config: EffectiveConfig,
pub(crate) thinking_levels: Vec<crate::thinking::ThinkingLevel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TuiRewindPlanPreview {
pub(crate) plan: crate::checkpoints::RestorePlan,
pub(crate) text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TuiRewindWorkerResult {
Changes {
text: String,
plans: Vec<TuiRewindPlanPreview>,
},
Plan {
plan: crate::checkpoints::RestorePlan,
text: String,
dry_run: bool,
mode: crate::checkpoints::RewindMode,
},
Forked {
session_id: String,
session: Session,
text: String,
},
Execution {
execution: crate::checkpoints::RewindExecution,
text: String,
session_record_error: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TuiExportWorkerResult {
pub(crate) report: crate::sessions::SessionExportReport,
}
#[derive(Clone)]
pub(crate) enum TuiEvent {
WorkerOutcomeReady,
Output(OutputEvent),
Activity(ActivityEvent),
Error(String),
ModelCatalog {
request_id: u64,
result: Result<crate::model_catalog::CatalogForUi, String>,
visible_rows: usize,
},
ModelSelectionFinished {
request_id: u64,
session_id: Option<String>,
generation: u64,
result: Box<Result<TuiModelSelectionResult, String>>,
},
ThemeCatalogLoaded {
request_id: u64,
result: Result<crate::appearance::ThemeCatalog, String>,
},
ThemePersisted {
request_id: u64,
theme_id: String,
revision: u64,
result: Result<(), String>,
},
FastModePersisted {
request_id: u64,
enabled: bool,
result: Result<(), String>,
},
UsageLoaded {
request_id: u64,
result: crate::tui::usage::UsageLoadResult,
},
OAuthInstructions {
worker_id: u64,
instructions: crate::login::LoginInstructions,
},
CompactionFinishedAuthoritative {
result: Result<Option<crate::compaction::CompactionResult>, String>,
activity: worker::CompactionActivityFinal,
runtime_config: Option<Box<worker::CompactionRuntimeRefresh>>,
},
OAuthFinished {
worker_id: u64,
provider_id: String,
result: Result<String, String>,
},
CustomProviderFinished {
worker_id: u64,
provider_id: String,
result: Result<String, String>,
},
Done,
RunCanceled {
prompt: String,
},
WorkerCompletionDeliveryFailed {
worker_id: u64,
error: String,
},
RunFinished {
worker_id: u64,
final_event: Option<Box<TuiEvent>>,
},
SessionTitleUpdated {
session_id: String,
title: String,
},
SessionPreviewLoaded {
request_id: u64,
session_id: String,
preview: state::SessionPreviewState,
},
SessionSwitchLoaded {
request_id: u64,
session_id: String,
result: Box<Result<SessionSwitchLoad, String>>,
},
RewindFinished {
request_id: u64,
session_id: String,
generation: u64,
result: Box<Result<TuiRewindWorkerResult, String>>,
},
ExportFinished {
request_id: u64,
session_id: String,
generation: u64,
result: Box<Result<TuiExportWorkerResult, String>>,
},
FooterGitBranchLoaded {
request_id: u64,
branch: Option<String>,
},
StartupCriticalLoaded {
request_id: u64,
result: Box<Result<TuiStartupCritical, String>>,
},
StartupDecorativeLoaded {
request_id: u64,
result: Box<Result<TuiStartupDecorative, String>>,
},
}
fn debug_result_status<T, E>(result: &Result<T, E>) -> &'static str {
if result.is_ok() { "ok" } else { "error" }
}
impl std::fmt::Debug for TuiEvent {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WorkerOutcomeReady => formatter.write_str("TuiEvent::WorkerOutcomeReady"),
Self::Output(_) => formatter
.debug_tuple("TuiEvent::Output")
.field(&"<redacted>")
.finish(),
Self::Activity(_) => formatter
.debug_tuple("TuiEvent::Activity")
.field(&"<redacted>")
.finish(),
Self::Error(_) => formatter
.debug_tuple("TuiEvent::Error")
.field(&"<redacted>")
.finish(),
Self::ModelCatalog {
request_id,
result,
visible_rows,
} => formatter
.debug_struct("TuiEvent::ModelCatalog")
.field("request_id", request_id)
.field("result", &debug_result_status(result))
.field("visible_rows", visible_rows)
.finish(),
Self::ModelSelectionFinished {
request_id,
generation,
result,
..
} => formatter
.debug_struct("TuiEvent::ModelSelectionFinished")
.field("request_id", request_id)
.field("generation", generation)
.field("result", &debug_result_status(result.as_ref()))
.finish(),
Self::ThemeCatalogLoaded { request_id, result } => formatter
.debug_struct("TuiEvent::ThemeCatalogLoaded")
.field("request_id", request_id)
.field("result", &debug_result_status(result))
.finish(),
Self::ThemePersisted {
request_id,
revision,
result,
..
} => formatter
.debug_struct("TuiEvent::ThemePersisted")
.field("request_id", request_id)
.field("revision", revision)
.field("result", &debug_result_status(result))
.finish(),
Self::FastModePersisted {
request_id,
enabled,
result,
} => formatter
.debug_struct("TuiEvent::FastModePersisted")
.field("request_id", request_id)
.field("enabled", enabled)
.field("result", &debug_result_status(result))
.finish(),
Self::UsageLoaded { request_id, .. } => formatter
.debug_struct("TuiEvent::UsageLoaded")
.field("request_id", request_id)
.field("result", &"<redacted>")
.finish(),
Self::OAuthInstructions { worker_id, .. } => formatter
.debug_struct("TuiEvent::OAuthInstructions")
.field("worker_id", worker_id)
.field("instructions", &"<redacted>")
.finish(),
Self::CompactionFinishedAuthoritative { result, .. } => formatter
.debug_struct("TuiEvent::CompactionFinishedAuthoritative")
.field("result", &debug_result_status(result))
.field("activity", &"<redacted>")
.finish(),
Self::OAuthFinished {
worker_id, result, ..
} => formatter
.debug_struct("TuiEvent::OAuthFinished")
.field("worker_id", worker_id)
.field("provider_id", &"<redacted>")
.field("result", &debug_result_status(result))
.finish(),
Self::CustomProviderFinished {
worker_id, result, ..
} => formatter
.debug_struct("TuiEvent::CustomProviderFinished")
.field("worker_id", worker_id)
.field("provider_id", &"<redacted>")
.field("result", &debug_result_status(result))
.finish(),
Self::Done => formatter.write_str("TuiEvent::Done"),
Self::RunCanceled { .. } => formatter
.debug_struct("TuiEvent::RunCanceled")
.field("prompt", &"<redacted>")
.finish(),
Self::WorkerCompletionDeliveryFailed { worker_id, .. } => formatter
.debug_struct("TuiEvent::WorkerCompletionDeliveryFailed")
.field("worker_id", worker_id)
.field("error", &"<redacted>")
.finish(),
Self::RunFinished {
worker_id,
final_event,
} => formatter
.debug_struct("TuiEvent::RunFinished")
.field("worker_id", worker_id)
.field("final_event", &final_event.as_ref().map(|_| "<redacted>"))
.finish(),
Self::SessionTitleUpdated { .. } => formatter
.debug_struct("TuiEvent::SessionTitleUpdated")
.field("session_id", &"<redacted>")
.field("title", &"<redacted>")
.finish(),
Self::SessionPreviewLoaded { request_id, .. } => formatter
.debug_struct("TuiEvent::SessionPreviewLoaded")
.field("request_id", request_id)
.field("session_id", &"<redacted>")
.field("preview", &"<redacted>")
.finish(),
Self::SessionSwitchLoaded {
request_id, result, ..
} => formatter
.debug_struct("TuiEvent::SessionSwitchLoaded")
.field("request_id", request_id)
.field("session_id", &"<redacted>")
.field("result", &debug_result_status(result.as_ref()))
.finish(),
Self::RewindFinished {
request_id,
generation,
result,
..
} => formatter
.debug_struct("TuiEvent::RewindFinished")
.field("request_id", request_id)
.field("generation", generation)
.field("result", &debug_result_status(result.as_ref()))
.finish(),
Self::ExportFinished {
request_id,
generation,
result,
..
} => formatter
.debug_struct("TuiEvent::ExportFinished")
.field("request_id", request_id)
.field("generation", generation)
.field("result", &debug_result_status(result.as_ref()))
.finish(),
Self::FooterGitBranchLoaded { request_id, .. } => formatter
.debug_struct("TuiEvent::FooterGitBranchLoaded")
.field("request_id", request_id)
.field("branch", &"<redacted>")
.finish(),
Self::StartupCriticalLoaded { request_id, result } => formatter
.debug_struct("TuiEvent::StartupCriticalLoaded")
.field("request_id", request_id)
.field("result", &debug_result_status(result.as_ref()))
.finish(),
Self::StartupDecorativeLoaded { request_id, result } => formatter
.debug_struct("TuiEvent::StartupDecorativeLoaded")
.field("request_id", request_id)
.field("result", &debug_result_status(result.as_ref()))
.finish(),
}
}
}
const CRITICAL_EVENT_TIMEOUT: Duration = Duration::from_millis(250);
const PROMPT_CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const FOOTER_GIT_BRANCH_REFRESH_INTERVAL: Duration = Duration::from_secs(2);
pub(crate) const INLINE_SEPARATOR_CHAR: char = '•';
pub(crate) const PADDED_INLINE_SEPARATOR: &str = " • ";
const LEGACY_PADDED_INLINE_SEPARATOR: &str = " · ";
pub(crate) fn split_once_inline_separator(text: &str) -> Option<(&str, &str)> {
text.split_once(PADDED_INLINE_SEPARATOR)
.or_else(|| text.split_once(LEGACY_PADDED_INLINE_SEPARATOR))
}
pub(crate) fn rsplit_once_inline_separator(text: &str) -> Option<(&str, &str)> {
text.rsplit_once(PADDED_INLINE_SEPARATOR)
.or_else(|| text.rsplit_once(LEGACY_PADDED_INLINE_SEPARATOR))
}
pub(crate) fn normalize_inline_separators(text: &str) -> String {
text.replace(LEGACY_PADDED_INLINE_SEPARATOR, PADDED_INLINE_SEPARATOR)
}
#[cfg(test)]
const CANCEL_JOIN_GRACE_PERIOD: Duration = Duration::from_millis(50);
#[cfg(test)]
const WORKER_EXIT_JOIN_TIMEOUT: Duration = CANCEL_JOIN_GRACE_PERIOD;
#[cfg(not(test))]
const WORKER_EXIT_JOIN_TIMEOUT: Duration = Duration::from_secs(2);
static NEXT_TUI_RUN_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) fn send_critical(sender: &Sender<TuiEvent>, event: TuiEvent) -> anyhow::Result<()> {
sender
.send_timeout(event, CRITICAL_EVENT_TIMEOUT)
.map_err(|error| anyhow::anyhow!("tui event channel blocked or closed: {error}"))
}
pub(crate) fn send_best_effort(sender: &Sender<TuiEvent>, event: TuiEvent) {
const BEST_EFFORT_QUEUE_RESERVE: usize = 64;
if sender.capacity().is_some_and(|capacity| {
capacity > BEST_EFFORT_QUEUE_RESERVE
&& sender.len() >= capacity.saturating_sub(BEST_EFFORT_QUEUE_RESERVE)
}) {
return;
}
match sender.try_send(event) {
Ok(()) | Err(crossbeam_channel::TrySendError::Full(_)) => {}
Err(crossbeam_channel::TrySendError::Disconnected(_)) => {}
}
}
pub(crate) fn run_tui(config: TuiSessionConfig) -> Result<Option<crate::updates::RestartContext>> {
install_terminal_restore_panic_hook();
let (sender, receiver) = bounded::<TuiEvent>(1024);
let mut app = MissionControlApp::new(config, sender);
run_with_cleanup(
&mut app,
|app| app.run(receiver),
|app| app.cleanup_after_run(),
)?;
app.finish_update_handoff()
}
pub(crate) fn run_with_cleanup(
app: &mut MissionControlApp,
run: impl FnOnce(&mut MissionControlApp) -> Result<()>,
cleanup: impl FnOnce(&mut MissionControlApp) -> Result<()>,
) -> Result<()> {
let run_result = run(app);
let cleanup_result = cleanup(app);
match (run_result, cleanup_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) => Err(error),
(Ok(()), Err(error)) => Err(anyhow::anyhow!("Mission Control cleanup failed: {error}")),
(Err(run_error), Err(cleanup_error)) => Err(anyhow::anyhow!(
"Mission Control run failed: {run_error}; cleanup failed: {cleanup_error}"
)),
}
}
fn install_terminal_restore_panic_hook() {
let previous_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
terminal::restore_terminal_best_effort();
previous_hook(panic_info);
}));
}
mod controller;
use controller::*;
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;