mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[2 of 2] Start fresh TUI thread in background (#23176)
## Why After the terminal-probe work in #23175, fresh-session startup still waits for `thread/start` before the chat input can become usable. The chat widget already has the machinery to hold early submissions until a session is configured, so fresh `thread/start` does not need to stay on the input-ready hot path. Refs #16335. ## What This PR starts fresh app-server threads in a background task, reports completion through a startup app event, and attaches the primary session once `thread/start` returns. Resume and fork startup paths remain synchronous. ## Benchmark In the local pty startup benchmark, this PR's pre-optimization base branch, #23175, measured about 152ms median from launch to accepted chat input. The stacked result measured about 66ms median, for an approximate additional savings of 85-95ms. For broader context, the original `main` baseline before either startup optimization was about 250.5ms median. We also measured Codex 0.117.0 on the same machine at about 64.6ms median, so the stacked branch is back in the old-startup-time range. ## Stack 1. [#23175: [1 of 2] Optimize TUI startup terminal probes](https://github.com/openai/codex/pull/23175) — base PR 2. [#23176: [2 of 2] Start fresh TUI thread in background](https://github.com/openai/codex/pull/23176) — this PR ## Verification - `cargo test -p codex-tui`
This commit is contained in:
committed by
GitHub
Unverified
parent
d4f842f3b3
commit
c0f7e1b99f
+32
-3
@@ -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: bool,
|
||||
// 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,27 @@ async fn resolve_runtime_model_provider_base_url(provider: &ModelProviderInfo) -
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_startup_thread_start(
|
||||
app_server: &AppServerSession,
|
||||
config: Config,
|
||||
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);
|
||||
tokio::spawn(async move {
|
||||
let result = crate::app_server_session::start_thread_with_request_handle(
|
||||
request_handle,
|
||||
config,
|
||||
thread_params_mode,
|
||||
remote_cwd_override,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("{err:#}"));
|
||||
app_event_tx.send(AppEvent::StartupThreadStarted { result });
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum ActiveTurnSteerRace {
|
||||
Missing,
|
||||
@@ -778,10 +800,14 @@ impl App {
|
||||
&initial_images,
|
||||
);
|
||||
let thread_and_widget_started_at = Instant::now();
|
||||
let pending_startup_thread_start = matches!(
|
||||
&session_selection,
|
||||
SessionSelection::StartFresh | SessionSelection::Exit
|
||||
);
|
||||
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.
|
||||
spawn_startup_thread_start(&app_server, config.clone(), 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 +837,9 @@ impl App {
|
||||
.clone(),
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
};
|
||||
(ChatWidget::new_with_app_event(init), Some(started))
|
||||
let mut chat_widget = ChatWidget::new_with_app_event(init);
|
||||
chat_widget.set_queue_submissions_until_session_configured(/*queue*/ true);
|
||||
(chat_widget, None)
|
||||
}
|
||||
SessionSelection::Resume(target_session) => {
|
||||
let resumed = app_server
|
||||
@@ -956,6 +984,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,
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
pending_hook_enabled_writes: HashMap::new(),
|
||||
};
|
||||
|
||||
@@ -23,6 +23,10 @@ impl App {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
AppEvent::StartupThreadStarted { result } => {
|
||||
self.handle_startup_thread_started(app_server, result)
|
||||
.await?;
|
||||
}
|
||||
AppEvent::ClearUi => {
|
||||
self.clear_terminal_ui(tui, /*redraw_header*/ false)?;
|
||||
self.reset_app_ui_state_after_clear();
|
||||
|
||||
@@ -418,10 +418,48 @@ impl App {
|
||||
self.primary_session_configured = None;
|
||||
self.pending_primary_events.clear();
|
||||
self.pending_app_server_requests.clear();
|
||||
self.pending_startup_thread_start = false;
|
||||
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,
|
||||
result: Result<AppServerStartedThread, String>,
|
||||
) -> Result<()> {
|
||||
if !self.pending_startup_thread_start {
|
||||
if let Ok(started) = result {
|
||||
let thread_id = started.session.thread_id;
|
||||
if let Err(err) = app_server.thread_unsubscribe(thread_id).await {
|
||||
tracing::warn!(
|
||||
thread_id = %thread_id,
|
||||
"failed to unsubscribe stale startup thread: {err}"
|
||||
);
|
||||
}
|
||||
self.discard_thread_local_state(thread_id).await;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.pending_startup_thread_start = false;
|
||||
self.chat_widget
|
||||
.set_queue_submissions_until_session_configured(/*queue*/ false);
|
||||
match result {
|
||||
Ok(started) => {
|
||||
self.enqueue_primary_thread_session(started.session, started.turns)
|
||||
.await?;
|
||||
self.chat_widget.maybe_send_next_queued_input();
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"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,
|
||||
|
||||
@@ -369,15 +369,15 @@ impl App {
|
||||
self.chat_widget.add_error_message(message);
|
||||
return false;
|
||||
}
|
||||
self.discard_side_thread_local(thread_id).await;
|
||||
self.discard_thread_local_state(thread_id).await;
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) async fn discard_closed_side_thread(&mut self, thread_id: ThreadId) {
|
||||
self.discard_side_thread_local(thread_id).await;
|
||||
self.discard_thread_local_state(thread_id).await;
|
||||
}
|
||||
|
||||
async fn discard_side_thread_local(&mut self, thread_id: ThreadId) {
|
||||
pub(super) async fn discard_thread_local_state(&mut self, thread_id: ThreadId) {
|
||||
self.abort_thread_event_listener(thread_id);
|
||||
self.thread_event_channels.remove(&thread_id);
|
||||
self.side_threads.remove(&thread_id);
|
||||
|
||||
@@ -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: false,
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
pending_hook_enabled_writes: HashMap::new(),
|
||||
}
|
||||
|
||||
@@ -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: false,
|
||||
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: false,
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
pending_hook_enabled_writes: HashMap::new(),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -132,6 +135,121 @@ fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_thread_started_submits_queued_startup_input() {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
app.pending_startup_thread_start = true;
|
||||
app.chat_widget
|
||||
.set_queue_submissions_until_session_configured(/*queue*/ true);
|
||||
app.chat_widget
|
||||
.apply_external_edit("queued before startup completes".to_string());
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(
|
||||
app.chat_widget.queued_user_message_texts(),
|
||||
vec!["queued before startup completes".to_string()]
|
||||
);
|
||||
|
||||
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(
|
||||
app.chat_widget.config_ref(),
|
||||
))
|
||||
.await
|
||||
.expect("embedded app server");
|
||||
let thread_id = ThreadId::new();
|
||||
app.handle_startup_thread_started(
|
||||
&mut app_server,
|
||||
Ok(AppServerStartedThread {
|
||||
session: test_thread_session(thread_id, test_path_buf("/tmp/project")),
|
||||
turns: Vec::new(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("startup thread should attach");
|
||||
|
||||
match next_user_turn_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "queued before startup completes".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected queued startup input submission, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_thread_start_failure_returns_error() {
|
||||
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
app.pending_startup_thread_start = true;
|
||||
|
||||
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(
|
||||
app.chat_widget.config_ref(),
|
||||
))
|
||||
.await
|
||||
.expect("embedded app server");
|
||||
let err = app
|
||||
.handle_startup_thread_started(&mut app_server, Err("boom".to_string()))
|
||||
.await
|
||||
.expect_err("startup thread failure should exit instead of leaving chat unconfigured");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("Failed to start a fresh session through the app server: boom")
|
||||
);
|
||||
assert!(!app.pending_startup_thread_start);
|
||||
assert_eq!(app.primary_thread_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_startup_thread_started_removes_local_routing_state() -> Result<()> {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.thread_stack_size(8 * 1024 * 1024)
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(async {
|
||||
let mut app = make_test_app().await;
|
||||
let mut app_server =
|
||||
crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?;
|
||||
let primary_thread_id = ThreadId::new();
|
||||
let stale_thread_id = ThreadId::new();
|
||||
app.primary_thread_id = Some(primary_thread_id);
|
||||
app.thread_event_channels.insert(
|
||||
primary_thread_id,
|
||||
ThreadEventChannel::new(THREAD_EVENT_CHANNEL_CAPACITY),
|
||||
);
|
||||
app.activate_thread_channel(primary_thread_id).await;
|
||||
app.thread_event_channels.insert(
|
||||
stale_thread_id,
|
||||
ThreadEventChannel::new(THREAD_EVENT_CHANNEL_CAPACITY),
|
||||
);
|
||||
app.agent_navigation.upsert(
|
||||
stale_thread_id,
|
||||
/*agent_nickname*/ None,
|
||||
/*agent_role*/ None,
|
||||
/*is_closed*/ false,
|
||||
);
|
||||
assert!(app.thread_event_channels.contains_key(&stale_thread_id));
|
||||
assert!(app.agent_navigation.get(&stale_thread_id).is_some());
|
||||
|
||||
app.handle_startup_thread_started(
|
||||
&mut app_server,
|
||||
Ok(AppServerStartedThread {
|
||||
session: test_thread_session(stale_thread_id, test_path_buf("/tmp/project")),
|
||||
turns: Vec::new(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(!app.thread_event_channels.contains_key(&stale_thread_id));
|
||||
assert_eq!(app.agent_navigation.get(&stale_thread_id), None);
|
||||
assert_eq!(app.active_thread_id, Some(primary_thread_id));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
@@ -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,11 @@ 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 {
|
||||
result: Result<AppServerStartedThread, String>,
|
||||
},
|
||||
|
||||
/// Clear the terminal UI (screen + scrollback), start a fresh session, and keep the
|
||||
/// previous chat resumable.
|
||||
ClearUi,
|
||||
|
||||
@@ -119,6 +119,7 @@ use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report {
|
||||
color_eyre::eyre::eyre!("{context}: {err}")
|
||||
@@ -167,6 +168,7 @@ impl ThreadParamsMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AppServerStartedThread {
|
||||
pub(crate) session: ThreadSessionState,
|
||||
pub(crate) turns: Vec<Turn>,
|
||||
@@ -337,6 +339,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 +430,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 +1005,27 @@ impl AppServerSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_thread_with_request_handle(
|
||||
request_handle: AppServerRequestHandle,
|
||||
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(format!("startup-thread-start-{}", Uuid::new_v4())),
|
||||
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>,
|
||||
|
||||
@@ -355,6 +355,7 @@ pub(crate) struct ChatComposer {
|
||||
attachments: AttachmentState,
|
||||
placeholder_text: String,
|
||||
is_task_running: bool,
|
||||
queue_submissions: bool,
|
||||
/// Slash-command draft staged for local recall after application-level dispatch.
|
||||
///
|
||||
/// This slot is intentionally separate from `ChatComposerHistory` so inline slash commands can
|
||||
@@ -524,6 +525,7 @@ impl ChatComposer {
|
||||
attachments: AttachmentState::default(),
|
||||
placeholder_text,
|
||||
is_task_running: false,
|
||||
queue_submissions: false,
|
||||
pending_slash_command_history: None,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
next_element_id: 0,
|
||||
@@ -2817,6 +2819,9 @@ impl ChatComposer {
|
||||
now: Instant,
|
||||
) -> (InputResult, bool) {
|
||||
if should_queue {
|
||||
if let Some(pasted) = self.draft.paste_burst.flush_before_modified_input() {
|
||||
self.handle_paste(pasted);
|
||||
}
|
||||
let raw_text = self.draft.textarea.text();
|
||||
let defer_slash_validation =
|
||||
self.should_parse_as_slash_on_dequeue_from_raw_text(raw_text);
|
||||
@@ -3227,13 +3232,13 @@ impl ChatComposer {
|
||||
self.footer.mode = reset_mode_after_activity(self.footer.mode);
|
||||
}
|
||||
if self.queue_keys.is_pressed(key_event)
|
||||
&& (self.is_task_running || !self.is_bang_shell_command())
|
||||
&& (self.is_task_running || self.queue_submissions || !self.is_bang_shell_command())
|
||||
{
|
||||
return self.handle_submission(self.is_task_running);
|
||||
return self.handle_submission(self.is_task_running || self.queue_submissions);
|
||||
}
|
||||
|
||||
if self.submit_keys.is_pressed(key_event) {
|
||||
return self.handle_submission(/*should_queue*/ false);
|
||||
return self.handle_submission(self.queue_submissions);
|
||||
}
|
||||
|
||||
if let KeyEvent {
|
||||
@@ -4143,6 +4148,10 @@ impl ChatComposer {
|
||||
self.is_task_running = running;
|
||||
}
|
||||
|
||||
pub(crate) fn set_queue_submissions(&mut self, queue_submissions: bool) {
|
||||
self.queue_submissions = queue_submissions;
|
||||
}
|
||||
|
||||
pub(crate) fn set_context_window(&mut self, percent: Option<i64>, used_tokens: Option<i64>) {
|
||||
if self.footer.context_window_percent == percent
|
||||
&& self.footer.context_window_used_tokens == used_tokens
|
||||
@@ -7064,6 +7073,49 @@ mod tests {
|
||||
assert_eq!(composer.draft.textarea.text(), "hi\nthere");
|
||||
}
|
||||
|
||||
/// Behavior: startup-pending submissions are queued immediately, so Enter should flush any
|
||||
/// buffered burst text into that queued message instead of turning into a draft newline.
|
||||
#[test]
|
||||
fn queued_submission_flushes_ascii_burst_instead_of_inserting_newline() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
let mut now = Instant::now();
|
||||
let step = Duration::from_millis(1);
|
||||
for ch in ['h', 'i'] {
|
||||
let _ = composer.handle_input_basic_with_time(
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
now,
|
||||
);
|
||||
now += step;
|
||||
}
|
||||
assert!(composer.is_in_paste_burst());
|
||||
|
||||
let (result, _) = composer.handle_submission_with_time(/*should_queue*/ true, now);
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
InputResult::Queued {
|
||||
text: "hi".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
action: QueuedInputAction::Plain,
|
||||
}
|
||||
);
|
||||
assert!(composer.draft.textarea.text().is_empty());
|
||||
assert!(!composer.is_in_paste_burst());
|
||||
}
|
||||
|
||||
/// Behavior: even if Enter suppression would normally be active for a burst, Enter should
|
||||
/// still dispatch a built-in slash command when the first line begins with `/`.
|
||||
#[test]
|
||||
@@ -8029,6 +8081,40 @@ mod tests {
|
||||
assert!(found_error, "expected error history cell to be sent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_queues_when_queue_submissions_is_enabled() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
composer.set_queue_submissions(/*queue_submissions*/ true);
|
||||
composer
|
||||
.draft
|
||||
.textarea
|
||||
.set_text_clearing_elements("queued before session");
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
InputResult::Queued {
|
||||
text: "queued before session".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
action: QueuedInputAction::Plain,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_queues_slash_led_prompts_while_task_running_without_validation() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -999,6 +999,10 @@ impl BottomPane {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_queue_submissions(&mut self, queue_submissions: bool) {
|
||||
self.composer.set_queue_submissions(queue_submissions);
|
||||
}
|
||||
|
||||
/// Hide the status indicator while leaving task-running state untouched.
|
||||
pub(crate) fn hide_status_indicator(&mut self) {
|
||||
if self.status.take().is_some() {
|
||||
|
||||
@@ -72,6 +72,11 @@ impl ChatWidget {
|
||||
self.queue_user_message_with_options(user_message, QueuedInputAction::Plain);
|
||||
}
|
||||
|
||||
pub(crate) fn set_queue_submissions_until_session_configured(&mut self, queue: bool) {
|
||||
self.bottom_pane
|
||||
.set_queue_submissions(queue && !self.is_session_configured());
|
||||
}
|
||||
|
||||
pub(super) fn queue_user_message_with_options(
|
||||
&mut self,
|
||||
user_message: UserMessage,
|
||||
|
||||
@@ -20,6 +20,8 @@ impl ChatWidget {
|
||||
self.session_network_proxy = session.network_proxy.clone();
|
||||
let previous_thread_id = self.thread_id;
|
||||
self.thread_id = Some(session.thread_id);
|
||||
self.bottom_pane
|
||||
.set_queue_submissions(/*queue_submissions*/ false);
|
||||
if previous_thread_id != self.thread_id {
|
||||
self.review.recent_auto_review_denials = RecentAutoReviewDenials::default();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user