Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ mod thread_goal_actions;
mod thread_routing;
mod thread_session_state;
mod thread_settings;
mod thread_title;
mod transcript_export;
mod working_directory;

Expand Down Expand Up @@ -595,6 +596,7 @@ pub(crate) struct App {
windows_sandbox: WindowsSandboxState,

thread_event_channels: HashMap<ThreadId, ThreadEventChannel>,
temporary_structured_requests: HashMap<ThreadId, mpsc::UnboundedSender<ServerNotification>>,
thread_event_listener_tasks: HashMap<ThreadId, JoinHandle<()>>,
agent_navigation: AgentNavigationState,
agents_overview: agents_overview::AgentsOverviewState,
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/tui/src/app/app_server_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ impl App {
let _ = self.dynamic_tool_status_updates.send(status.clone());
}

// Hidden helper threads must not enter visible thread routing or overview refreshes.
if let ServerNotificationThreadTarget::Thread(thread_id) =
server_notification_thread_target(&notification)
&& let Some(sender) = self.temporary_structured_requests.get(&thread_id)
{
if matches!(
&notification,
ServerNotification::ItemCompleted(_) | ServerNotification::TurnCompleted(_)
) && sender.send(notification).is_err()
{
self.temporary_structured_requests.remove(&thread_id);
}

return;
}

if let ServerNotification::ThreadStarted(started) = &notification
&& let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id, ..
Expand Down
59 changes: 53 additions & 6 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use super::resize_reflow::trailing_run_start;
use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::app_event::ThreadTitleDestination;
use crate::app_server_session::ForkGoalContinuation;
use crate::app_server_session::UnsupportedLegacyPermissionProfile;
use crate::app_server_session::turn_permissions_overrides;
Expand Down Expand Up @@ -2459,13 +2460,59 @@ impl App {
.await;
}
AppEvent::RenameAgentsOverviewThread { thread_id, name } => {
if let Err(error) = app_server.thread_set_name(thread_id, name.clone()).await {
if let Ok(mut state) = self.agents_overview.view_state.lock() {
state.input = name;
state.renaming = true;
match app_server.thread_set_name(thread_id, name.clone()).await {
Ok(()) => self.chat_widget.expect_manual_thread_name(thread_id, name),
Err(error) => {
if let Ok(mut state) = self.agents_overview.view_state.lock() {
state.input = name;
state.renaming = true;
}
self.chat_widget
.add_error_message(format!("Failed to rename task: {error}"));
}
}
}
AppEvent::ThreadTitleStarted {
thread_id,
destination,
prompt,
effort,
result,
} => {
self.on_thread_title_started(
app_server,
thread_id,
destination,
prompt,
effort,
result,
);
}
AppEvent::GeneratedThreadTitle {
thread_id,
temporary_thread_id,
destination,
result,
} => {
self.temporary_structured_requests
.remove(&temporary_thread_id);

match destination {
ThreadTitleDestination::Automatic { expected_title } => {
if let Ok(response) = result
&& let Some(title) = super::thread_title::parse_thread_title(&response)
&& self.chat_widget.thread_id() == Some(thread_id)
&& self.chat_widget.thread_name().as_deref()
== Some(expected_title.as_str())
{
match app_server.thread_set_name(thread_id, title.clone()).await {
Ok(()) => self.chat_widget.expect_automatic_thread_name(title),
Err(error) => {
tracing::debug!(%error, "failed to apply generated thread title");
}
}
}
}
self.chat_widget
.add_error_message(format!("Failed to rename task: {error}"));
}
}
AppEvent::StopAgentsOverviewThread { thread_id } => {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ See the Codex keymap documentation for supported actions and examples."
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
thread_event_channels: HashMap::new(),
temporary_structured_requests: HashMap::new(),
thread_event_listener_tasks: HashMap::new(),
agent_navigation: AgentNavigationState::default(),
agents_overview: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub(super) async fn make_test_app() -> App {
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
thread_event_channels: HashMap::new(),
temporary_structured_requests: HashMap::new(),
thread_event_listener_tasks: HashMap::new(),
agent_navigation: AgentNavigationState::default(),
agents_overview: Default::default(),
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5447,6 +5447,7 @@ async fn make_test_app() -> App {
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
thread_event_channels: HashMap::new(),
temporary_structured_requests: HashMap::new(),
thread_event_listener_tasks: HashMap::new(),
agent_navigation: AgentNavigationState::default(),
agents_overview: Default::default(),
Expand Down Expand Up @@ -5526,6 +5527,7 @@ async fn make_test_app_with_channels() -> (
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
thread_event_channels: HashMap::new(),
temporary_structured_requests: HashMap::new(),
thread_event_listener_tasks: HashMap::new(),
agent_navigation: AgentNavigationState::default(),
agents_overview: Default::default(),
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/tui/src/app/tests/safety_buffering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const RETRY_PROMPT: &str = "Handle the safety-buffered request";
const COMMITTED_STEER: &str = "Keep the accepted steer";
const UNSENT_DRAFT: &str = "Keep this unsent draft";
const RETRY_GOAL: &str = "Preserve this goal across the retry";
const SAFETY_RETRY_THREAD_NAME: &str = "Safety retry source";

#[derive(Clone, Copy, PartialEq, Eq)]
enum SafetyRetryScenario {
Expand Down Expand Up @@ -420,8 +421,13 @@ goals = true

let mut tui = crate::tui::test_support::make_test_tui()?;
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let started = app_server.start_thread(&app.config).await?;
let mut started = app_server.start_thread(&app.config).await?;
let source_thread_id = started.session.thread_id;
// Keep ordered response fixtures focused on safety retries, not background naming.
app_server
.thread_set_name(source_thread_id, SAFETY_RETRY_THREAD_NAME.to_string())
.await?;
started.session.thread_name = Some(SAFETY_RETRY_THREAD_NAME.to_string());
app.replace_chat_widget_with_app_server_thread(
&mut tui,
started,
Expand Down
59 changes: 56 additions & 3 deletions codex-rs/tui/src/app/thread_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::app_event::ThreadTitleDestination;
use crate::chatwidget::ThreadInputStateRestoreMode;
use crate::session_resume::read_session_model;
use codex_app_server_protocol::ThreadStartedNotification;
Expand Down Expand Up @@ -781,9 +782,9 @@ impl App {
Ok(true)
}
AppCommand::SetThreadName { name } => {
app_server
.thread_set_name(thread_id, name.to_string())
.await?;
let name = name.to_string();
app_server.thread_set_name(thread_id, name.clone()).await?;
self.chat_widget.expect_manual_thread_name(thread_id, name);
Ok(true)
}
AppCommand::Review { target } => {
Expand Down Expand Up @@ -1796,9 +1797,61 @@ impl App {
// thread, so unrelated shutdowns cannot consume this marker.
self.pending_shutdown_exit_thread_id = None;
}
let automatic_title_user_message = if self.chat_widget.thread_name().is_none()
&& let ThreadBufferedEvent::Notification(notification) = &event
&& let ServerNotification::ItemCompleted(notification) = notification.as_ref()
&& let ThreadItem::UserMessage { content, .. } = &notification.item
{
Some(
content
.iter()
.filter_map(|item| match item {
codex_app_server_protocol::UserInput::Text { text, .. } => {
Some(crate::ide_context::extract_prompt_request_with_offset(text).0)
}
_ => None,
})
.collect::<Vec<_>>()
.join(" "),
)
} else {
None
};
let had_active_view = self.chat_widget.has_active_view();
self.handle_thread_event_now_recovering_file_changes(event)
.await;
if let Some(user_message) = automatic_title_user_message {
let expected_title = user_message
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(super::thread_title::THREAD_TITLE_MAX_CHARS)
.collect::<String>();

if !expected_title.is_empty()
&& let Some(thread_id) = self.active_thread_id
{
match app_server
.thread_set_name(thread_id, expected_title.clone())
.await
{
Ok(()) => {
self.chat_widget
.expect_automatic_thread_name(expected_title.clone());
self.generate_thread_title(
app_server,
thread_id,
ThreadTitleDestination::Automatic { expected_title },
super::thread_title::thread_title_prompt(&user_message),
);
}
Err(error) => {
tracing::debug!(%error, "failed to set provisional thread title");
}
}
}
}
if !had_active_view
&& self.chat_widget.has_active_view()
&& self.startup_protected_input_boundary
Expand Down
Loading
Loading