Start fresh TUI thread in background

This commit is contained in:
Eric Traut
2026-05-17 10:48:13 -07:00
parent e43a2e297f
commit ac0cdf81a2
7 changed files with 117 additions and 4 deletions

View File

@@ -543,6 +543,7 @@ pub(crate) struct App {
primary_session_configured: Option<ThreadSessionState>,
pending_primary_events: VecDeque<ThreadBufferedEvent>,
pending_app_server_requests: PendingAppServerRequests,
pending_startup_thread_start_request_id: Option<String>,
// Serialize plugin enablement writes per plugin so stale completions cannot
// overwrite a newer toggle, even if the plugin is toggled from different
// cwd contexts.
@@ -575,6 +576,33 @@ async fn resolve_runtime_model_provider_base_url(provider: &ModelProviderInfo) -
}
}
fn spawn_startup_thread_start(
app_server: &AppServerSession,
config: Config,
request_id: String,
app_event_tx: AppEventSender,
) {
let request_handle = app_server.request_handle();
let thread_params_mode = app_server.thread_params_mode();
let remote_cwd_override = app_server.remote_cwd_override().map(Path::to_path_buf);
let event_request_id = request_id.clone();
tokio::spawn(async move {
let result = crate::app_server_session::start_thread_with_request_handle(
request_handle,
request_id,
config,
thread_params_mode,
remote_cwd_override,
)
.await
.map_err(|err| format!("{err:#}"));
app_event_tx.send(AppEvent::StartupThreadStarted {
request_id: event_request_id,
result,
});
});
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ActiveTurnSteerRace {
Missing,
@@ -778,10 +806,20 @@ impl App {
&initial_images,
);
let thread_and_widget_started_at = Instant::now();
let mut pending_startup_thread_start_request_id = None;
let (mut chat_widget, initial_started_thread) = match session_selection {
SessionSelection::StartFresh | SessionSelection::Exit => {
let started = app_server.start_thread(&config).await?;
// Only count a startup tooltip once the fresh thread can actually render it.
let startup_thread_start_request_id =
format!("startup-thread-start-{}", Uuid::new_v4());
pending_startup_thread_start_request_id =
Some(startup_thread_start_request_id.clone());
spawn_startup_thread_start(
&app_server,
config.clone(),
startup_thread_start_request_id,
app_event_tx.clone(),
);
// Count a startup tooltip once the initial chat widget can render it.
let startup_tooltip_override =
prepare_startup_tooltip_override(&mut config, &available_models, is_first_run)
.await;
@@ -811,7 +849,7 @@ impl App {
.clone(),
session_telemetry: session_telemetry.clone(),
};
(ChatWidget::new_with_app_event(init), Some(started))
(ChatWidget::new_with_app_event(init), None)
}
SessionSelection::Resume(target_session) => {
let resumed = app_server
@@ -956,6 +994,7 @@ See the Codex keymap documentation for supported actions and examples."
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_startup_thread_start_request_id,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
};

View File

@@ -23,6 +23,10 @@ impl App {
)
.await;
}
AppEvent::StartupThreadStarted { request_id, result } => {
self.handle_startup_thread_started(app_server, request_id, result)
.await?;
}
AppEvent::ClearUi => {
self.clear_terminal_ui(tui, /*redraw_header*/ false)?;
self.reset_app_ui_state_after_clear();

View File

@@ -418,10 +418,46 @@ impl App {
self.primary_session_configured = None;
self.pending_primary_events.clear();
self.pending_app_server_requests.clear();
self.pending_startup_thread_start_request_id = None;
self.chat_widget.set_pending_thread_approvals(Vec::new());
self.sync_active_agent_label();
}
pub(super) async fn handle_startup_thread_started(
&mut self,
app_server: &mut AppServerSession,
request_id: String,
result: Result<AppServerStartedThread, String>,
) -> Result<()> {
if self.pending_startup_thread_start_request_id.as_deref() != Some(request_id.as_str()) {
if let Ok(started) = result
&& let Err(err) = app_server
.thread_unsubscribe(started.session.thread_id)
.await
{
tracing::warn!(
thread_id = %started.session.thread_id,
"failed to unsubscribe stale startup thread: {err}"
);
}
return Ok(());
}
self.pending_startup_thread_start_request_id = None;
match result {
Ok(started) => {
self.enqueue_primary_thread_session(started.session, started.turns)
.await?;
}
Err(err) => {
self.chat_widget.add_error_message(format!(
"Failed to start a fresh session through the app server: {err}"
));
}
}
Ok(())
}
pub(super) async fn start_fresh_session_with_summary_hint(
&mut self,
tui: &mut tui::Tui,

View File

@@ -60,6 +60,7 @@ pub(super) async fn make_test_app() -> App {
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_startup_thread_start_request_id: None,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
}

View File

@@ -3882,6 +3882,7 @@ async fn make_test_app() -> App {
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_startup_thread_start_request_id: None,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
}
@@ -3945,6 +3946,7 @@ async fn make_test_app_with_channels() -> (
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
pending_app_server_requests: PendingAppServerRequests::default(),
pending_startup_thread_start_request_id: None,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
},

View File

@@ -33,6 +33,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::ApprovalPreset;
use crate::app_command::AppCommand;
use crate::app_server_session::AppServerStartedThread;
use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::StatusLineItem;
use crate::bottom_pane::TerminalTitleItem;
@@ -180,6 +181,12 @@ pub(crate) enum AppEvent {
/// Start a new session.
NewSession,
/// Result of the fresh startup thread that is attached after the input UI is live.
StartupThreadStarted {
request_id: String,
result: Result<AppServerStartedThread, String>,
},
/// Clear the terminal UI (screen + scrollback), start a fresh session, and keep the
/// previous chat resumable.
ClearUi,

View File

@@ -167,6 +167,7 @@ impl ThreadParamsMode {
}
}
#[derive(Debug)]
pub(crate) struct AppServerStartedThread {
pub(crate) session: ThreadSessionState,
pub(crate) turns: Vec<Turn>,
@@ -337,6 +338,7 @@ impl AppServerSession {
self.client.next_event().await
}
#[cfg(test)]
pub(crate) async fn start_thread(&mut self, config: &Config) -> Result<AppServerStartedThread> {
self.start_thread_with_session_start_source(config, /*session_start_source*/ None)
.await
@@ -427,7 +429,7 @@ impl AppServerSession {
Ok(started)
}
fn thread_params_mode(&self) -> ThreadParamsMode {
pub(crate) fn thread_params_mode(&self) -> ThreadParamsMode {
self.thread_params_mode
}
@@ -1002,6 +1004,28 @@ impl AppServerSession {
}
}
pub(crate) async fn start_thread_with_request_handle(
request_handle: AppServerRequestHandle,
request_id: String,
config: Config,
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<PathBuf>,
) -> Result<AppServerStartedThread> {
let response: ThreadStartResponse = request_handle
.request_typed(ClientRequest::ThreadStart {
request_id: RequestId::String(request_id),
params: thread_start_params_from_config(
&config,
thread_params_mode,
remote_cwd_override.as_deref(),
/*session_start_source*/ None,
),
})
.await
.map_err(|err| bootstrap_request_error("thread/start failed during TUI bootstrap", err))?;
started_thread_from_start_response(response, &config, thread_params_mode).await
}
fn thread_realtime_start_params(
thread_id: ThreadId,
transport: Option<ThreadRealtimeStartTransport>,