mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Keep the composer responsive during Command Center session creation (#46077)
## Why Creating a session from Command Center waits for configuration and server requests before displaying the composer. Scanning loaded agents also adds unnecessary round trips for a new session with no descendants. ## What changed - Show an editable startup composer while loading settings, starting the thread, and attaching the new session. - Transfer the draft into the new session, including pending paste state, and retain edits for a retry if setup fails. - Keep session creation running when cancellation keys are pressed in the provisional composer. - Skip descendant backfill and paused-goal resume prompts for new sessions. GitOrigin-RevId: 8371a6c3edc09de6366253021eae99f7b60d78ef
This commit is contained in:
@@ -21,6 +21,7 @@ use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap;
|
||||
use crate::chatwidget::ThreadInputStateRestoreMode;
|
||||
use crate::startup_draft::StartupDraftPump;
|
||||
use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
@@ -54,6 +55,7 @@ pub(super) struct AgentsOverviewState {
|
||||
/// Keep new tasks subscribed and reusable until a first turn makes them resumable.
|
||||
pub(super) blank_sessions: HashMap<ThreadId, crate::app_server_session::AppServerStartedThread>,
|
||||
pub(super) input_states: HashMap<ThreadId, ThreadInputState>,
|
||||
pub(super) new_session_draft: Option<Box<StartupDraftPump>>,
|
||||
pub(super) dispatched_requests: HashMap<ThreadId, Vec<ServerRequest>>,
|
||||
}
|
||||
|
||||
@@ -310,8 +312,10 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppRunControl> {
|
||||
Box::pin(self.attach_agents_overview_thread(tui, app_server, thread_id, /*started*/ None))
|
||||
.await
|
||||
Box::pin(self.attach_agents_overview_thread(
|
||||
tui, app_server, thread_id, /*started*/ None, /*startup_draft*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn attach_agents_overview_thread(
|
||||
@@ -320,10 +324,12 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
root_thread_id: ThreadId,
|
||||
started: Option<(Config, crate::app_server_session::AppServerStartedThread)>,
|
||||
mut startup_draft: Option<&mut StartupDraftPump>,
|
||||
) -> color_eyre::Result<AppRunControl> {
|
||||
if self.windows_sandbox_blocks_thread_switch() {
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let is_new_session = started.is_some();
|
||||
if self.current_displayed_thread_id() == Some(root_thread_id)
|
||||
&& (!self.thread_unavailable(root_thread_id)
|
||||
|| self.chat_widget.is_external_writer_view())
|
||||
@@ -392,9 +398,12 @@ impl App {
|
||||
.insert(active_thread_id, input_state);
|
||||
}
|
||||
|
||||
let target_thread = match app_server
|
||||
.thread_read(root_thread_id, /*include_turns*/ false)
|
||||
.await
|
||||
let target_thread = match StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft.as_deref_mut(),
|
||||
tui,
|
||||
app_server.thread_read(root_thread_id, /*include_turns*/ false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(thread) => thread,
|
||||
Err(error) => {
|
||||
@@ -456,6 +465,7 @@ impl App {
|
||||
&mut resume_config,
|
||||
target_thread.cwd.as_path(),
|
||||
Some(&target_thread),
|
||||
/*startup_draft*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -553,7 +563,18 @@ impl App {
|
||||
};
|
||||
if !previous_running_thread_ids.is_empty() {
|
||||
for side_thread_id in Vec::from_iter(self.side_threads.keys().copied()) {
|
||||
if !self.discard_side_thread(app_server, side_thread_id).await {
|
||||
let discarded = match startup_draft.as_deref_mut() {
|
||||
Some(draft) => {
|
||||
draft
|
||||
.run_until(
|
||||
tui,
|
||||
self.discard_side_thread(app_server, side_thread_id),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => self.discard_side_thread(app_server, side_thread_id).await,
|
||||
};
|
||||
if !discarded {
|
||||
let _ = app_server.thread_unsubscribe(root_thread_id).await;
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
@@ -580,7 +601,14 @@ impl App {
|
||||
&& !previous_displayed_thread_id
|
||||
.is_some_and(|id| self.agents_overview.blank_sessions.contains_key(&id))
|
||||
{
|
||||
self.shutdown_current_thread(app_server).await;
|
||||
match startup_draft.as_deref_mut() {
|
||||
Some(draft) => {
|
||||
draft
|
||||
.run_until(tui, self.shutdown_current_thread(app_server))
|
||||
.await?
|
||||
}
|
||||
None => self.shutdown_current_thread(app_server).await,
|
||||
}
|
||||
}
|
||||
// Explicit choices carry across cold resumes and new sessions.
|
||||
self.runtime_approval_policy_override =
|
||||
@@ -656,10 +684,13 @@ impl App {
|
||||
.matches_config(&self.config))
|
||||
.then(|| RuntimePermissionProfileOverride::from_restored_config(&self.config));
|
||||
}
|
||||
if !self
|
||||
.backfill_loaded_subagent_threads(app_server)
|
||||
.await
|
||||
.completed
|
||||
// A new session has no descendants. Scanning every loaded thread here
|
||||
// adds a serial round trip per agent before the composer can render.
|
||||
if !is_new_session
|
||||
&& !self
|
||||
.backfill_loaded_subagent_threads(app_server)
|
||||
.await
|
||||
.completed
|
||||
{
|
||||
self.backfill_loaded_subagent_threads(app_server).await;
|
||||
}
|
||||
@@ -668,7 +699,12 @@ impl App {
|
||||
&& thread_id != root_thread_id
|
||||
&& Some(thread_id) != previous_displayed_thread_id
|
||||
&& !self.agents_overview.blank_sessions.contains_key(&thread_id)
|
||||
&& let Err(error) = app_server.thread_unsubscribe(thread_id).await
|
||||
&& let Err(error) = StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft.as_deref_mut(),
|
||||
tui,
|
||||
app_server.thread_unsubscribe(thread_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(%thread_id, %error, "failed to unsubscribe previous agent thread");
|
||||
}
|
||||
@@ -705,7 +741,7 @@ impl App {
|
||||
self.chat_widget.maybe_send_next_queued_input();
|
||||
}
|
||||
}
|
||||
if !read_only {
|
||||
if !read_only && !is_new_session {
|
||||
self.maybe_prompt_resume_paused_goal_after_resume(app_server, root_thread_id)
|
||||
.await;
|
||||
}
|
||||
@@ -738,6 +774,7 @@ impl App {
|
||||
tui: &mut tui::Tui,
|
||||
app_server: &mut AppServerSession,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
mut startup_draft: Option<&mut StartupDraftPump>,
|
||||
) -> Option<(Config, Option<PathBuf>)> {
|
||||
if self
|
||||
.chat_widget
|
||||
@@ -763,7 +800,13 @@ impl App {
|
||||
|cwd| cwd.to_path_buf(),
|
||||
)
|
||||
};
|
||||
let mut config = match self.rebuild_config_for_cwd(local_cwd).await {
|
||||
let mut config = match StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft.as_deref_mut(),
|
||||
tui,
|
||||
self.rebuild_config_for_cwd(local_cwd),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.add_agents_overview_error(format!("Failed to load project settings: {error}"));
|
||||
@@ -781,6 +824,7 @@ impl App {
|
||||
&mut config,
|
||||
&trust_cwd,
|
||||
/*resumed_thread*/ None,
|
||||
startup_draft.as_deref_mut(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
@@ -812,10 +856,17 @@ impl App {
|
||||
.or_else(|| app_server.remote_cwd_override())
|
||||
.unwrap_or(Path::new(".")),
|
||||
};
|
||||
if let Some(draft) = startup_draft.as_deref_mut() {
|
||||
draft.apply_config(&config);
|
||||
}
|
||||
let mut server_model_cleared = false;
|
||||
match crate::config_update::read_effective_config_if_supported(
|
||||
app_server.request_handle(),
|
||||
defaults_cwd,
|
||||
match StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft,
|
||||
tui,
|
||||
crate::config_update::read_effective_config_if_supported(
|
||||
app_server.request_handle(),
|
||||
defaults_cwd,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
//! Worktrees start at the repository default branch and bind only the new session.
|
||||
|
||||
use super::*;
|
||||
use crate::startup_draft::StartupDraftInitialScreen;
|
||||
use crate::startup_draft::StartupDraftPump;
|
||||
use crate::startup_draft::StartupDraftSessionAction;
|
||||
|
||||
/// Owns a freshly created checkout until the UI accepts its completion event.
|
||||
/// Dropping a cancelled worker or undelivered event removes only a clean checkout.
|
||||
@@ -39,13 +42,46 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
) -> Result<AppRunControl> {
|
||||
if self.reconnect.offline || self.windows_sandbox_blocks_thread_switch() {
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let previous_thread = self.current_displayed_thread_id();
|
||||
let mut draft = self
|
||||
.agents_overview
|
||||
.new_session_draft
|
||||
.take()
|
||||
.unwrap_or_else(|| {
|
||||
Box::new(StartupDraftPump::new(
|
||||
tui,
|
||||
StartupDraftInitialScreen::Composer,
|
||||
StartupDraftSessionAction::NewFromCommandCenter,
|
||||
))
|
||||
});
|
||||
let mut display_config = self.chat_widget.config_ref().clone();
|
||||
if let Some(cwd) = cwd.as_ref() {
|
||||
display_config.cwd = cwd.clone();
|
||||
}
|
||||
draft.apply_config(&display_config);
|
||||
tui.terminal.clear()?;
|
||||
// Keep the large session-start future off the TUI's stack in dev builds.
|
||||
Box::pin(
|
||||
self.start_agents_overview_session(
|
||||
tui, app_server, cwd, /*managed_worktree*/ None,
|
||||
),
|
||||
)
|
||||
.await
|
||||
let result = Box::pin(self.start_agents_overview_session(
|
||||
tui,
|
||||
app_server,
|
||||
cwd,
|
||||
/*managed_worktree*/ None,
|
||||
Some(&mut draft),
|
||||
))
|
||||
.await;
|
||||
if self.current_displayed_thread_id() != previous_thread {
|
||||
draft.flush_pending_paste_newline(tui).await?;
|
||||
self.chat_widget.restore_startup_draft(draft.take_draft());
|
||||
} else {
|
||||
// Retain edits if setup fails so retrying `n` does not lose the draft.
|
||||
draft.flush_pending_events(tui).await?;
|
||||
self.agents_overview.new_session_draft = Some(draft);
|
||||
}
|
||||
tui.frame_requester().schedule_frame();
|
||||
result
|
||||
}
|
||||
|
||||
pub(in crate::app) async fn start_agents_overview_session(
|
||||
@@ -57,6 +93,7 @@ impl App {
|
||||
codex_worktree::WorktreeManager,
|
||||
codex_worktree::ManagedWorktree,
|
||||
)>,
|
||||
mut startup_draft: Option<&mut StartupDraftPump>,
|
||||
) -> Result<AppRunControl> {
|
||||
if self.reconnect.offline || self.windows_sandbox_blocks_thread_switch() {
|
||||
if let Some((_, checkout)) = &managed_worktree {
|
||||
@@ -68,7 +105,7 @@ impl App {
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let Some((config, remote_cwd)) = self
|
||||
.agents_overview_session_config(tui, app_server, cwd)
|
||||
.agents_overview_session_config(tui, app_server, cwd, startup_draft.as_deref_mut())
|
||||
.await
|
||||
else {
|
||||
if let Some((_, checkout)) = &managed_worktree {
|
||||
@@ -94,15 +131,22 @@ impl App {
|
||||
display_label: active.id,
|
||||
})
|
||||
});
|
||||
let result = app_server
|
||||
.start_thread_with_session_start_source(
|
||||
&crate::local_settings::LocalSettings::from(&config),
|
||||
let local_settings = crate::local_settings::LocalSettings::from(&config);
|
||||
if let Some(draft) = startup_draft.as_deref_mut() {
|
||||
draft.apply_config(&config);
|
||||
}
|
||||
let result = StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft.as_deref_mut(),
|
||||
tui,
|
||||
Box::pin(app_server.start_thread_with_session_start_source(
|
||||
&local_settings,
|
||||
&config,
|
||||
/*session_start_source*/ None,
|
||||
remote_cwd.as_deref(),
|
||||
selected_profile.as_ref(),
|
||||
)
|
||||
.await;
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
let started = match result {
|
||||
Ok(started) => started,
|
||||
Err(error) => {
|
||||
@@ -144,9 +188,14 @@ impl App {
|
||||
.insert(thread_id, started.clone());
|
||||
// Use the dashboard's existing attachment path, which preserves running agents
|
||||
// and unsent input in the previous session. Do not send an initial turn.
|
||||
let control = self
|
||||
.attach_agents_overview_thread(tui, app_server, thread_id, Some((config, started)))
|
||||
.await?;
|
||||
let control = Box::pin(self.attach_agents_overview_thread(
|
||||
tui,
|
||||
app_server,
|
||||
thread_id,
|
||||
Some((config, started)),
|
||||
startup_draft,
|
||||
))
|
||||
.await?;
|
||||
if self.current_displayed_thread_id() != Some(thread_id) {
|
||||
self.agents_overview.blank_sessions.remove(&thread_id);
|
||||
let _ = app_server.thread_unsubscribe(thread_id).await;
|
||||
@@ -184,7 +233,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
let Some((config, _)) = self
|
||||
.agents_overview_session_config(tui, app_server, cwd)
|
||||
.agents_overview_session_config(tui, app_server, cwd, /*startup_draft*/ None)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
|
||||
@@ -2472,7 +2472,7 @@ impl App {
|
||||
};
|
||||
let manager = pending.manager.clone();
|
||||
let cwd = AbsolutePathBuf::try_from(checkout.cwd.clone())?;
|
||||
return Box::pin(self.start_agents_overview_session(tui, app_server, Some(cwd), Some((manager, checkout)))).await;
|
||||
return Box::pin(self.start_agents_overview_session(tui, app_server, Some(cwd), Some((manager, checkout)), /*startup_draft*/ None)).await;
|
||||
}
|
||||
Err(error) => self.add_agents_overview_error(error),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use super::*;
|
||||
use crate::onboarding::onboarding_screen::check_directory_trust;
|
||||
use crate::startup_draft::StartupDraftPump;
|
||||
use crate::startup_hooks_review::StartupHooksReviewOutcome;
|
||||
use crate::startup_hooks_review::load_startup_hooks_review_entry;
|
||||
use crate::startup_hooks_review::maybe_run_startup_hooks_review;
|
||||
@@ -135,6 +136,7 @@ impl App {
|
||||
&mut resume_config.0,
|
||||
&trust_cwd,
|
||||
resumed_thread.as_ref(),
|
||||
/*startup_draft*/ None,
|
||||
)
|
||||
.await?;
|
||||
resume_config.1 = crate::local_settings::LocalSettings::from(&resume_config.0);
|
||||
@@ -148,6 +150,7 @@ impl App {
|
||||
config: &mut Config,
|
||||
cwd: &Path,
|
||||
resumed_thread: Option<&codex_app_server_protocol::Thread>,
|
||||
mut startup_draft: Option<&mut StartupDraftPump>,
|
||||
) -> std::result::Result<(), AppRunControl> {
|
||||
// Keep the existing explicit remote --cd gate, including retries after cancellation.
|
||||
// Other remote destinations await authoritative trust-root metadata.
|
||||
@@ -166,7 +169,7 @@ impl App {
|
||||
&self.app_server_target,
|
||||
cwd,
|
||||
resumed_thread,
|
||||
/*startup_draft*/ None,
|
||||
startup_draft.as_deref_mut(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
@@ -187,21 +190,40 @@ impl App {
|
||||
return Err(AppRunControl::Continue);
|
||||
}
|
||||
if result.directory_trust_persisted && !app_server.uses_remote_workspace() {
|
||||
*config = self
|
||||
.rebuild_config_for_cwd(config.cwd.to_path_buf())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
self.add_session_picker_error(format!(
|
||||
"Failed to reload trusted folder settings: {error}"
|
||||
));
|
||||
AppRunControl::Continue
|
||||
})?;
|
||||
*config = StartupDraftPump::run_with_optional_draft(
|
||||
startup_draft.as_deref_mut(),
|
||||
tui,
|
||||
self.rebuild_config_for_cwd(config.cwd.to_path_buf()),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
self.add_session_picker_error(format!(
|
||||
"Failed to reload trusted folder settings: {error}"
|
||||
));
|
||||
AppRunControl::Continue
|
||||
})?;
|
||||
if resumed_thread.is_none() {
|
||||
let hooks = load_startup_hooks_review_entry(
|
||||
let load_hooks = load_startup_hooks_review_entry(
|
||||
app_server.request_handle(),
|
||||
config.cwd.to_path_buf(),
|
||||
)
|
||||
.await;
|
||||
);
|
||||
let hooks = if let Some(draft) = startup_draft {
|
||||
draft.apply_config(config);
|
||||
async {
|
||||
let hooks = draft.run_until(tui, load_hooks).await?;
|
||||
draft.flush_pending_events(tui).await?;
|
||||
Ok::<_, std::io::Error>(hooks)
|
||||
}
|
||||
.await
|
||||
.map_err(|error| {
|
||||
self.add_session_picker_error(format!(
|
||||
"Unable to load folder hooks: {error}"
|
||||
));
|
||||
AppRunControl::Continue
|
||||
})?
|
||||
} else {
|
||||
load_hooks.await
|
||||
};
|
||||
match maybe_run_startup_hooks_review(
|
||||
app_server,
|
||||
tui,
|
||||
|
||||
@@ -45,6 +45,7 @@ fn trust_launch_folder(app: &mut App) {
|
||||
#[tokio::test]
|
||||
async fn command_center_new_reads_server_defaults_for_actual_destination() -> Result<()> {
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
for (mode, explicit_cwd, launch_override, expected_cwd, expected_model) in [
|
||||
("local", false, false, "launch", "server-model"),
|
||||
("local", true, false, "destination", "destination-model"),
|
||||
@@ -263,6 +264,7 @@ async fn command_center_new_reads_server_defaults_for_actual_destination() -> Re
|
||||
#[tokio::test]
|
||||
async fn command_center_new_preserves_explicit_choices_and_managed_defaults() -> Result<()> {
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
for (choice, expected_model, expected_effort) in [
|
||||
("saved", "server-model", "high"),
|
||||
("cli_effort", "server-model", "low"),
|
||||
@@ -362,6 +364,7 @@ async fn command_center_new_preserves_explicit_choices_and_managed_defaults() ->
|
||||
#[tokio::test]
|
||||
async fn command_center_new_read_failure_keeps_overview_and_does_not_start() -> Result<()> {
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
for capability in [
|
||||
HistoryCapabilities::ConfigReadFails,
|
||||
HistoryCapabilities::ThreadStartFails,
|
||||
@@ -459,6 +462,7 @@ async fn command_center_new_preserves_permissions_across_sessions() -> Result<()
|
||||
.await
|
||||
);
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
for _ in 0..2 {
|
||||
app.new_agents_overview_session(&mut tui, &mut server, /*cwd*/ None)
|
||||
.await?;
|
||||
@@ -545,6 +549,7 @@ async fn command_center_new_preserves_only_selected_server_profiles() -> Result<
|
||||
RuntimePermissionProfileOverride::from_restored_config(app.chat_widget.config_ref()),
|
||||
);
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
app.new_agents_overview_session(&mut tui, &mut server, /*cwd*/ None)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
@@ -647,6 +652,7 @@ async fn command_center_new_restores_blank_drafts_and_builtin_permissions() -> R
|
||||
)
|
||||
.await?;
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
app.new_agents_overview_session(&mut tui, &mut server, /*cwd*/ None)
|
||||
.await?;
|
||||
let first = app.chat_widget.thread_id().unwrap();
|
||||
@@ -855,6 +861,7 @@ async fn command_center_new_checkout_and_worktree_preserve_source_and_default_br
|
||||
)
|
||||
.await?;
|
||||
let mut tui = make_test_tui()?;
|
||||
tui.pause_events();
|
||||
let new_session = app.new_agents_overview_session(
|
||||
&mut tui,
|
||||
&mut server,
|
||||
@@ -958,6 +965,7 @@ async fn command_center_new_checkout_and_worktree_preserve_source_and_default_br
|
||||
&mut failed_server,
|
||||
Some(unused.cwd.clone().abs()),
|
||||
Some((manager.clone(), unused.clone())),
|
||||
/*startup_draft*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(app.chat_widget.thread_id(), Some(second));
|
||||
|
||||
@@ -61,6 +61,8 @@ pub(crate) enum StartupDraftInitialScreen {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum StartupDraftSessionAction {
|
||||
New,
|
||||
/// Keep in-app creation alive while allowing draft edits until setup finishes.
|
||||
NewFromCommandCenter,
|
||||
Resume,
|
||||
Fork,
|
||||
}
|
||||
@@ -111,25 +113,11 @@ impl StartupDraft {
|
||||
initialized_terminal.enhanced_keys_supported,
|
||||
initialized_terminal.stderr_guard,
|
||||
);
|
||||
let (app_event_tx, app_event_rx) = unbounded_channel();
|
||||
let bottom_pane = startup_draft_bottom_pane(
|
||||
AppEventSender::new(app_event_tx),
|
||||
tui.frame_requester(),
|
||||
tui.enhanced_keys_supported(),
|
||||
);
|
||||
let events = tui.event_stream();
|
||||
let pump = StartupDraftPump::new(&tui, initial_screen, session_action);
|
||||
let mut draft = Self {
|
||||
tui,
|
||||
terminal_restore_guard,
|
||||
pump: StartupDraftPump {
|
||||
header: startup_session_header(/*config*/ None),
|
||||
bottom_pane,
|
||||
events,
|
||||
app_event_rx,
|
||||
initial_screen,
|
||||
session_action,
|
||||
pending_paste_newline: None,
|
||||
},
|
||||
pump,
|
||||
};
|
||||
draft.pump.show_initial_screen(&mut draft.tui)?;
|
||||
Ok(draft)
|
||||
@@ -165,6 +153,43 @@ impl StartupDraft {
|
||||
}
|
||||
|
||||
impl StartupDraftPump {
|
||||
pub(crate) fn new(
|
||||
tui: &Tui,
|
||||
initial_screen: StartupDraftInitialScreen,
|
||||
session_action: StartupDraftSessionAction,
|
||||
) -> Self {
|
||||
let (app_event_tx, app_event_rx) = unbounded_channel();
|
||||
Self {
|
||||
header: startup_session_header(/*config*/ None),
|
||||
bottom_pane: startup_draft_bottom_pane(
|
||||
AppEventSender::new(app_event_tx),
|
||||
tui.frame_requester(),
|
||||
tui.enhanced_keys_supported(),
|
||||
),
|
||||
events: tui.event_stream(),
|
||||
app_event_rx,
|
||||
initial_screen,
|
||||
session_action,
|
||||
pending_paste_newline: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep a provisional composer responsive when the caller has one to display.
|
||||
pub(crate) async fn run_with_optional_draft<F, T, E>(
|
||||
draft: Option<&mut Self>,
|
||||
tui: &mut Tui,
|
||||
future: F,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: Future<Output = Result<T, E>>,
|
||||
E: From<io::Error>,
|
||||
{
|
||||
match draft {
|
||||
Some(draft) => draft.run_until(tui, future).await?,
|
||||
None => future.await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh the session header and safe editor shortcuts without enabling modal editing.
|
||||
pub(crate) fn apply_config(&mut self, config: &Config) {
|
||||
let local_settings = crate::local_settings::LocalSettings::from(config);
|
||||
@@ -264,6 +289,10 @@ impl StartupDraftPump {
|
||||
|
||||
/// Preserve the editable draft, its cursor, and any pending large-paste placeholders.
|
||||
pub(crate) fn into_draft(mut self) -> ComposerDraftSnapshot {
|
||||
self.take_draft()
|
||||
}
|
||||
|
||||
pub(crate) fn take_draft(&mut self) -> ComposerDraftSnapshot {
|
||||
self.bottom_pane.flush_composer_paste_burst();
|
||||
self.bottom_pane.composer_draft_snapshot()
|
||||
}
|
||||
@@ -346,16 +375,22 @@ impl StartupDraftPump {
|
||||
self.pending_paste_newline = Some((Instant::now(), "\n".to_string()));
|
||||
}
|
||||
}
|
||||
handle_startup_draft_key(&mut self.bottom_pane, key).inspect_err(|error| {
|
||||
if StartupCancelled::matches(error)
|
||||
&& let Err(clear_error) = tui.terminal.clear()
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %clear_error,
|
||||
"failed to clear the cancelled startup composer"
|
||||
);
|
||||
match handle_startup_draft_key(&mut self.bottom_pane, key) {
|
||||
Err(error) if StartupCancelled::matches(&error) => {
|
||||
// In-app setup owns a live creation request. Keep pumping input
|
||||
// instead of abandoning it or blocking on cancellation cleanup.
|
||||
if self.session_action != StartupDraftSessionAction::NewFromCommandCenter {
|
||||
if let Err(clear_error) = tui.terminal.clear() {
|
||||
tracing::warn!(
|
||||
error = %clear_error,
|
||||
"failed to clear the cancelled startup composer"
|
||||
);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
result => result?,
|
||||
}
|
||||
}
|
||||
TuiEvent::Paste(text) => {
|
||||
if self.initial_screen == StartupDraftInitialScreen::Composer {
|
||||
@@ -477,7 +512,7 @@ fn startup_draft_renderable<'a>(
|
||||
let mut renderable = FlexRenderable::new();
|
||||
renderable.push(/*flex*/ 1, RenderableItem::Borrowed(header));
|
||||
let loading_message = match session_action {
|
||||
StartupDraftSessionAction::New => None,
|
||||
StartupDraftSessionAction::New | StartupDraftSessionAction::NewFromCommandCenter => None,
|
||||
StartupDraftSessionAction::Resume => Some(" Resuming session…"),
|
||||
StartupDraftSessionAction::Fork => Some(" Forking session…"),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user