mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Keep agents navigation available after a thread is archived (#53201)
## Why After a thread was archived externally, the TUI still advertised the Left-arrow agents shortcut, but pressing it in an empty composer did not open the agents view. ## What changed Reuse the agents navigation handler when connected to an unavailable thread. Preserve the existing empty-composer and editor guards, and require a key press so repeated Left-arrow events do not open the view. ## Testing Add a regression test for external thread archiving that verifies draft editing, the shortcut hint, ignored key repeats, and opening the agents command center, with a snapshot of the resulting view. GitOrigin-RevId: a3f36b42bafee948ed7f5842fcb425e414d7e024
This commit is contained in:
@@ -49,6 +49,100 @@ async fn agents_navigation_requires_local_daemon() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn externally_archived_thread_keeps_advertised_agents_navigation() -> Result<()> {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
let mut app_server = start_config_write_test_app_server(&app).await?;
|
||||
app.app_server_target = AppServerTarget::LocalDaemon {
|
||||
endpoint: crate::RemoteAppServerEndpoint::UnixSocket {
|
||||
socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
|
||||
},
|
||||
allow_embedded_fallback: true,
|
||||
};
|
||||
let init = app.chatwidget_init_for_forked_or_resumed_thread(
|
||||
&mut tui,
|
||||
app.config.clone(),
|
||||
/*initial_user_message*/ None,
|
||||
);
|
||||
app.replace_chat_widget(ChatWidget::new_with_app_event(init));
|
||||
let id = ThreadId::new();
|
||||
app.active_thread_id = Some(id);
|
||||
app.primary_thread_id = Some(id);
|
||||
app.chat_widget
|
||||
.handle_thread_session(test_thread_session(id, app.config.cwd.to_path_buf()));
|
||||
app.ensure_thread_channel(id);
|
||||
app.handle_app_server_event(
|
||||
&app_server,
|
||||
codex_app_server_client::AppServerEvent::ServerNotification(Box::new(
|
||||
ServerNotification::ThreadArchived(ThreadArchivedNotification {
|
||||
thread_id: id.to_string(),
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
while events.try_recv().is_ok() {}
|
||||
|
||||
// Left must still edit an existing draft before it can navigate from an empty composer.
|
||||
app.chat_widget
|
||||
.restore_user_message_to_composer("ab".into());
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
TuiEvent::Key(KeyCode::Left.into()),
|
||||
)
|
||||
.await?;
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
TuiEvent::Key(KeyCode::Char('!').into()),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "a!b");
|
||||
assert!(
|
||||
!std::iter::from_fn(|| events.try_recv().ok())
|
||||
.any(|event| matches!(event, AppEvent::OpenAgentsOverview))
|
||||
);
|
||||
for key in [
|
||||
KeyCode::End.into(),
|
||||
KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
|
||||
] {
|
||||
app.handle_tui_event(&mut tui, &mut app_server, TuiEvent::Key(key))
|
||||
.await?;
|
||||
}
|
||||
assert!(app.chat_widget.composer_is_empty());
|
||||
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 96).contains("for agents"));
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
TuiEvent::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Repeat,
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!std::iter::from_fn(|| events.try_recv().ok())
|
||||
.any(|event| matches!(event, AppEvent::OpenAgentsOverview))
|
||||
);
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
TuiEvent::Key(KeyCode::Left.into()),
|
||||
)
|
||||
.await?;
|
||||
let event = events.try_recv()?;
|
||||
assert_matches!(event, AppEvent::OpenAgentsOverview);
|
||||
app.handle_event(&mut tui, &mut app_server, event).await?;
|
||||
insta::assert_snapshot!(
|
||||
"externally_archived_thread_agents",
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 96)
|
||||
);
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_windows_sandbox_setup_blocks_thread_replacement() -> Result<()> {
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tui/src/app/tests/agents_navigation_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 96)"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
All 0 Needs you 0 Working 0 Ready 0 Inactive 0 tab/⇧tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Loading tasks… │
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
? help esc back ↑/↓ move enter open n new
|
||||
@@ -163,6 +163,7 @@
|
||||
//! intact until reconnection succeeds, except for the local `/warnings` command.
|
||||
//! When connected but the thread is unavailable, configured
|
||||
//! submit keys may dispatch explicitly allowed recovery and local commands; other drafts stay put.
|
||||
//! Empty-prompt agents navigation remains available with its usual editor and draft guards.
|
||||
//! Collapsed pastes expand into editable text so the full draft can be copied before quitting.
|
||||
//!
|
||||
//! # Reasoning Effort Animations
|
||||
@@ -3785,9 +3786,7 @@ impl ChatComposer {
|
||||
return false;
|
||||
}
|
||||
|
||||
if key_hint::plain(KeyCode::Left).is_press(*key_event) && self.agents_navigation_available()
|
||||
{
|
||||
self.app_event_tx.send(AppEvent::OpenAgentsOverview);
|
||||
if self.handle_agents_navigation(*key_event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,15 @@ impl ChatComposer {
|
||||
&& !self.popup_active()
|
||||
&& !self.draft.textarea.is_vim_operator_pending()
|
||||
}
|
||||
|
||||
pub(super) fn handle_agents_navigation(&mut self, key: KeyEvent) -> bool {
|
||||
if key.kind == KeyEventKind::Press
|
||||
&& key_hint::plain(KeyCode::Left).is_press(key)
|
||||
&& self.agents_navigation_available()
|
||||
{
|
||||
self.app_event_tx.send(AppEvent::OpenAgentsOverview);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Restricted editing retains drafts and allows the local warnings viewer.
|
||||
//! Unavailable threads also allow recovery and other local commands.
|
||||
//! Connected unavailable threads retain the empty-prompt agents shortcut.
|
||||
//! Paste Enter handling is shared with normal submission so buffered newlines survive both paths.
|
||||
//! Recovery commands must occupy one line and be visible before the submit key expands pastes.
|
||||
//! Offline draft edits also consume the Astra sparkle opportunity before rendering.
|
||||
@@ -56,6 +57,9 @@ impl ChatComposer {
|
||||
key: KeyEvent,
|
||||
mode: RestrictedInputMode,
|
||||
) -> InputResult {
|
||||
if mode == RestrictedInputMode::UnavailableThread && self.handle_agents_navigation(key) {
|
||||
return InputResult::None;
|
||||
}
|
||||
self.cancel_history_search();
|
||||
self.attachments.clear_remote_image_selection();
|
||||
self.popups.active = ActivePopup::None;
|
||||
|
||||
Reference in New Issue
Block a user