diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 482b3c770d..f47086e616 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -20,6 +20,7 @@ use crate::chatwidget::ChatWidget; use crate::chatwidget::ExternalEditorState; use crate::chatwidget::ReplayKind; use crate::chatwidget::ThreadInputState; +use crate::chatwidget::extract_first_bold; use crate::cwd_prompt::CwdPromptAction; use crate::diff_render::DiffSummary; use crate::exec_command::split_command_string; @@ -28,6 +29,9 @@ use crate::external_editor; use crate::file_search::FileSearchManager; use crate::history_cell; use crate::history_cell::HistoryCell; +use crate::history_cell::SubagentPanelAgent; +use crate::history_cell::SubagentPanelState; +use crate::history_cell::SubagentStatusCell; #[cfg(not(debug_assertions))] use crate::history_cell::UpdateAvailableHistoryCell; use crate::model_catalog::ModelCatalog; @@ -45,6 +49,7 @@ use crate::render::renderable::Renderable; use crate::resume_picker::SessionSelection; #[cfg(test)] use crate::test_support::PathBufExt; +use crate::text_formatting::truncate_text; use crate::tui; use crate::tui::TuiEvent; use crate::update_action::UpdateAction; @@ -54,6 +59,9 @@ use codex_app_server_client::AppServerRequestHandle; use codex_app_server_client::TypedRequestError; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo; +use codex_app_server_protocol::CollabAgentState; +use codex_app_server_protocol::CollabAgentStatus; +use codex_app_server_protocol::CollabAgentTool; use codex_app_server_protocol::ConfigLayerSource; use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; @@ -101,7 +109,17 @@ use codex_protocol::openai_models::ModelAvailabilityNux; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; +use codex_protocol::protocol::AgentMessageDeltaEvent; +use codex_protocol::protocol::AgentMessageEvent; +use codex_protocol::protocol::AgentSpawnMode as CollabAgentSpawnMode; +use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::CollabAgentSpawnEndEvent; +use codex_protocol::protocol::CollabCloseEndEvent; +use codex_protocol::protocol::CollabWaitingEndEvent; +use codex_protocol::protocol::ErrorEvent; +use codex_protocol::protocol::Event; +use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FinalOutput; use codex_protocol::protocol::GetHistoryEntryResponseEvent; use codex_protocol::protocol::ListSkillsResponseEvent; @@ -112,6 +130,9 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillErrorInfo; use codex_protocol::protocol::TokenUsage; +use codex_protocol::protocol::TurnAbortedEvent; +use codex_protocol::protocol::TurnCompleteEvent; +use codex_protocol::protocol::TurnStartedEvent; use codex_terminal_detection::user_agent; use codex_utils_absolute_path::AbsolutePathBuf; use color_eyre::eyre::Result; @@ -125,10 +146,12 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; use std::collections::BTreeMap; use std::collections::HashMap; +use std::collections::HashSet; use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use std::sync::Mutex as StdMutex; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::thread; @@ -205,6 +228,20 @@ fn command_execution_decision_to_review_decision( } } +fn app_server_collab_state_to_agent_status(state: &CollabAgentState) -> AgentStatus { + match state.status { + CollabAgentStatus::PendingInit => AgentStatus::PendingInit, + CollabAgentStatus::Running => AgentStatus::Running, + CollabAgentStatus::Completed => AgentStatus::Completed(state.message.clone()), + CollabAgentStatus::Errored => { + AgentStatus::Errored(state.message.clone().unwrap_or_default()) + } + CollabAgentStatus::Interrupted => AgentStatus::Interrupted, + CollabAgentStatus::Shutdown => AgentStatus::Shutdown, + CollabAgentStatus::NotFound => AgentStatus::NotFound, + } +} + /// Extracts `receiver_thread_ids` from collab agent tool-call notifications. /// /// Only `ItemStarted` and `ItemCompleted` notifications with a `CollabAgentToolCall` item carry @@ -1140,6 +1177,7 @@ fn terminal_summary(status: &AgentStatus) -> String { AgentStatus::Errored(message) => { truncate_text(message.trim(), SUBAGENT_UPDATE_PREVIEW_BUDGET) } + AgentStatus::Interrupted => "interrupted".to_string(), AgentStatus::Shutdown => "shutdown".to_string(), AgentStatus::NotFound => "not found".to_string(), AgentStatus::PendingInit | AgentStatus::Running => "running".to_string(), @@ -1468,6 +1506,8 @@ pub(crate) struct App { /// Controls the animation thread that sends CommitTick events. pub(crate) commit_anim_running: Arc, + /// Controls the animation thread that updates the live subagent panel. + pub(crate) subagent_anim_running: Arc, // Shared across ChatWidget instances so invalid status-line config warnings only emit once. status_line_invalid_items_warned: Arc, // Shared across ChatWidget instances so invalid terminal-title config warnings only emit once. @@ -1501,6 +1541,7 @@ pub(crate) struct App { thread_event_channels: HashMap, thread_event_listener_tasks: HashMap>, + subagents: SubagentRegistry, agent_navigation: AgentNavigationState, active_thread_id: Option, active_thread_rx: Option>, @@ -2074,6 +2115,7 @@ impl App { self.active_thread_id = Some(thread_id); self.active_thread_rx = receiver; self.refresh_pending_thread_approvals().await; + self.sync_subagent_panel_state(); } async fn store_active_thread_receiver(&mut self) { @@ -2110,6 +2152,158 @@ impl App { } self.active_thread_rx = None; self.refresh_pending_thread_approvals().await; + self.sync_subagent_panel_state(); + } + + fn subagents_root_active(&self) -> bool { + self.primary_thread_id.is_some() && self.active_thread_id == self.primary_thread_id + } + + fn emit_or_queue_subagent_history(&mut self, cell: Box) { + if self.subagents_root_active() { + self.app_event_tx.send(AppEvent::InsertHistoryCell(cell)); + } else { + self.subagents.queue_history(cell); + } + } + + fn flush_subagent_history_if_root_active(&mut self) { + if !self.subagents_root_active() { + return; + } + let pending = self.subagents.take_pending_history(); + for cell in pending { + self.app_event_tx.send(AppEvent::InsertHistoryCell(cell)); + } + } + + fn update_subagent_animation(&mut self, root_active: bool) { + let should_run = root_active && self.subagents.has_animating_agents(); + let is_running = self.subagent_anim_running.load(Ordering::Relaxed); + if should_run && !is_running { + self.app_event_tx.send(AppEvent::StartSubagentAnimation); + } else if !should_run && is_running { + self.app_event_tx.send(AppEvent::StopSubagentAnimation); + } + } + + fn sync_subagent_panel_state(&mut self) { + let root_active = self.subagents_root_active(); + self.subagents.rebuild_panel_state(); + + if root_active { + self.flush_subagent_history_if_root_active(); + if let Some(panel) = self.subagents.panel_cell() { + self.app_event_tx.send(AppEvent::UpdateSubagentPanel(panel)); + } else { + self.app_event_tx.send(AppEvent::ClearSubagentPanel); + } + } else { + self.app_event_tx.send(AppEvent::ClearSubagentPanel); + } + + self.update_subagent_animation(root_active); + } + + fn process_subagent_side_effects(&mut self, thread_id: ThreadId, event: &Event) { + if self.primary_thread_id == Some(thread_id) { + self.subagents.set_root_thread(thread_id); + } + + if self.subagents.is_root_thread(thread_id) { + match &event.msg { + EventMsg::CollabAgentSpawnEnd(ev) => { + let _ = self.subagents.on_spawn_end(ev); + } + EventMsg::CollabWaitingEnd(ev) => { + self.subagents.on_wait_end(ev); + } + EventMsg::CollabCloseEnd(ev) => { + let _ = self.subagents.on_close_end(ev); + } + _ => {} + } + } else { + let updates = self.subagents.on_agent_event(thread_id, &event.msg); + for cell in updates { + self.emit_or_queue_subagent_history(cell); + } + } + + self.sync_subagent_panel_state(); + } + + fn process_subagent_notification_side_effects( + &mut self, + thread_id: ThreadId, + notification: &ServerNotification, + ) { + if self.primary_thread_id == Some(thread_id) { + self.subagents.set_root_thread(thread_id); + } + + if !self.subagents.is_root_thread(thread_id) { + return; + } + + let item = match notification { + ServerNotification::ItemStarted(notification) => ¬ification.item, + ServerNotification::ItemCompleted(notification) => ¬ification.item, + _ => { + self.sync_subagent_panel_state(); + return; + } + }; + + let ThreadItem::CollabAgentToolCall { + id, + tool: CollabAgentTool::SpawnAgent, + sender_thread_id, + receiver_thread_ids, + prompt, + agents_states, + .. + } = item + else { + self.sync_subagent_panel_state(); + return; + }; + + let Some(new_thread_id) = receiver_thread_ids + .first() + .and_then(|thread_id| ThreadId::from_string(thread_id).ok()) + else { + self.sync_subagent_panel_state(); + return; + }; + + let sender_thread_id = ThreadId::from_string(sender_thread_id).unwrap_or(thread_id); + let entry = self.agent_navigation.get(&new_thread_id); + let spawn_mode = if entry.and_then(|entry| entry.agent_role.as_deref()) == Some("watchdog") + { + CollabAgentSpawnMode::Watchdog + } else { + CollabAgentSpawnMode::Spawn + }; + let status = agents_states + .get(&new_thread_id.to_string()) + .map(app_server_collab_state_to_agent_status) + .unwrap_or(AgentStatus::PendingInit); + + let _ = self.subagents.on_spawn_end(&CollabAgentSpawnEndEvent { + call_id: id.clone(), + sender_thread_id, + new_thread_id: Some(new_thread_id), + new_agent_nickname: entry.and_then(|entry| entry.agent_nickname.clone()), + new_agent_role: entry.and_then(|entry| entry.agent_role.clone()), + prompt: prompt.clone().unwrap_or_default(), + model: String::new(), + reasoning_effort: ReasoningEffortConfig::Medium, + spawn_mode, + status, + }); + + self.sync_subagent_panel_state(); } async fn note_thread_outbound_op(&mut self, thread_id: ThreadId, op: &AppCommand) { @@ -3392,7 +3586,9 @@ impl App { fn reset_thread_event_state(&mut self) { self.abort_all_thread_event_listeners(); + self.subagent_anim_running.store(false, Ordering::Release); self.thread_event_channels.clear(); + self.subagents = SubagentRegistry::new(self.config.animations); self.agent_navigation.clear(); self.active_thread_id = None; self.active_thread_rx = None; @@ -3401,6 +3597,7 @@ impl App { self.primary_session_configured = None; self.pending_primary_events.clear(); self.pending_app_server_requests.clear(); + self.chat_widget.clear_subagent_panel(); self.chat_widget.set_pending_thread_approvals(Vec::new()); self.sync_active_agent_label(); } @@ -3897,6 +4094,7 @@ impl App { let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone()); #[cfg(not(debug_assertions))] let upgrade_version = crate::updates::get_upgrade_version(&config); + let animations_enabled = config.animations; let mut app = Self { model_catalog, @@ -3916,6 +4114,7 @@ impl App { deferred_history_lines: Vec::new(), has_emitted_history_lines: false, commit_anim_running: Arc::new(AtomicBool::new(false)), + subagent_anim_running: Arc::new(AtomicBool::new(false)), status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), terminal_title_invalid_items_warned: terminal_title_invalid_items_warned.clone(), backtrack: BacktrackState::default(), @@ -3929,6 +4128,7 @@ impl App { windows_sandbox: WindowsSandboxState::default(), thread_event_channels: HashMap::new(), thread_event_listener_tasks: HashMap::new(), + subagents: SubagentRegistry::new(animations_enabled), agent_navigation: AgentNavigationState::default(), active_thread_id: None, active_thread_rx: None, @@ -4404,6 +4604,38 @@ impl App { AppEvent::CommitTick => { self.chat_widget.on_commit_tick(); } + AppEvent::StartSubagentAnimation => { + if self + .subagent_anim_running + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + let tx = self.app_event_tx.clone(); + let running = self.subagent_anim_running.clone(); + thread::spawn(move || { + while running.load(Ordering::Relaxed) { + thread::sleep(SUBAGENT_ANIMATION_TICK); + tx.send(AppEvent::SubagentTick); + } + }); + } + } + AppEvent::StopSubagentAnimation => { + self.subagent_anim_running.store(false, Ordering::Release); + } + AppEvent::SubagentTick => { + let root_active = self.subagents_root_active(); + self.update_subagent_animation(root_active); + if root_active && self.subagents.has_animating_agents() { + self.chat_widget.on_subagent_tick(); + } + } + AppEvent::UpdateSubagentPanel(panel) => { + self.chat_widget.on_subagent_panel_updated(panel); + } + AppEvent::ClearSubagentPanel => { + self.chat_widget.clear_subagent_panel(); + } AppEvent::Exit(mode) => { return Ok(self.handle_exit_mode(app_server, mode).await); } @@ -5804,6 +6036,9 @@ impl App { if let ThreadBufferedEvent::Notification(notification) = &event { self.hydrate_collab_agent_metadata_for_notification(app_server, notification) .await; + if let Some(active_thread_id) = self.active_thread_id { + self.process_subagent_notification_side_effects(active_thread_id, notification); + } } self.handle_thread_event_now(event); @@ -8967,6 +9202,7 @@ guardian_approval = true let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let session_telemetry = test_session_telemetry(&config, model.as_str()); + let animations_enabled = config.animations; App { model_catalog: chat_widget.model_catalog(), @@ -8986,6 +9222,7 @@ guardian_approval = true has_emitted_history_lines: false, enhanced_keys_supported: false, commit_anim_running: Arc::new(AtomicBool::new(false)), + subagent_anim_running: Arc::new(AtomicBool::new(false)), status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)), terminal_title_invalid_items_warned: Arc::new(AtomicBool::new(false)), backtrack: BacktrackState::default(), @@ -8999,6 +9236,7 @@ guardian_approval = true windows_sandbox: WindowsSandboxState::default(), thread_event_channels: HashMap::new(), thread_event_listener_tasks: HashMap::new(), + subagents: SubagentRegistry::new(animations_enabled), agent_navigation: AgentNavigationState::default(), active_thread_id: None, active_thread_rx: None, @@ -9020,6 +9258,7 @@ guardian_approval = true let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let session_telemetry = test_session_telemetry(&config, model.as_str()); + let animations_enabled = config.animations; ( App { @@ -9040,6 +9279,7 @@ guardian_approval = true has_emitted_history_lines: false, enhanced_keys_supported: false, commit_anim_running: Arc::new(AtomicBool::new(false)), + subagent_anim_running: Arc::new(AtomicBool::new(false)), status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)), terminal_title_invalid_items_warned: Arc::new(AtomicBool::new(false)), backtrack: BacktrackState::default(), @@ -9053,6 +9293,7 @@ guardian_approval = true windows_sandbox: WindowsSandboxState::default(), thread_event_channels: HashMap::new(), thread_event_listener_tasks: HashMap::new(), + subagents: SubagentRegistry::new(animations_enabled), agent_navigation: AgentNavigationState::default(), active_thread_id: None, active_thread_rx: None, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 2ae175013b..7fe07a993d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -9,6 +9,7 @@ //! quits without reaching into the app loop or coupling to shutdown/exit sequencing. use std::path::PathBuf; +use std::sync::Arc; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::PluginInstallResponse; @@ -30,6 +31,7 @@ use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::TerminalTitleItem; use crate::history_cell::HistoryCell; +use crate::history_cell::SubagentStatusCell; use codex_core::config::types::ApprovalsReviewer; use codex_features::Feature; @@ -271,6 +273,11 @@ pub(crate) enum AppEvent { StartCommitAnimation, StopCommitAnimation, CommitTick, + StartSubagentAnimation, + StopSubagentAnimation, + SubagentTick, + UpdateSubagentPanel(Arc), + ClearSubagentPanel, /// Update the current reasoning effort in the running app and widget. UpdateReasoningEffort(Option), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 346f3250c2..672c781996 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -50,6 +50,7 @@ use crate::bottom_pane::StatusLinePreviewData; use crate::bottom_pane::StatusLineSetupView; use crate::bottom_pane::TerminalTitleItem; use crate::bottom_pane::TerminalTitleSetupView; +use crate::history_cell::SubagentStatusCell; use crate::mention_codec::LinkedMention; use crate::mention_codec::encode_history_mentions; use crate::model_catalog::ModelCatalog; @@ -4544,211 +4545,9 @@ impl ChatWidget { self.had_work_activity = true; } -<<<<<<< HEAD pub(crate) fn new_with_app_event(common: ChatWidgetInit) -> Self { Self::new_with_op_target(common, CodexOpTarget::AppEvent) -======= - pub(crate) fn new(common: ChatWidgetInit, thread_manager: Arc) -> Self { - let ChatWidgetInit { - config, - frame_requester, - app_event_tx, - initial_user_message, - enhanced_keys_supported, - auth_manager, - models_manager, - feedback, - is_first_run, - feedback_audience, - model, - startup_tooltip_override, - status_line_invalid_items_warned, - terminal_title_invalid_items_warned, - session_telemetry, - } = common; - let model = model.filter(|m| !m.trim().is_empty()); - let mut config = config; - config.model = model.clone(); - let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep); - let mut rng = rand::rng(); - let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string(); - let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager); - - let model_override = model.as_deref(); - let model_for_header = model - .clone() - .unwrap_or_else(|| DEFAULT_MODEL_DISPLAY_NAME.to_string()); - let active_collaboration_mask = - Self::initial_collaboration_mask(&config, models_manager.as_ref(), model_override); - let header_model = active_collaboration_mask - .as_ref() - .and_then(|mask| mask.model.clone()) - .unwrap_or_else(|| model_for_header.clone()); - let fallback_default = Settings { - model: header_model.clone(), - reasoning_effort: None, - developer_instructions: None, - }; - // Collaboration modes start in Default mode. - let current_collaboration_mode = CollaborationMode { - mode: ModeKind::Default, - settings: fallback_default, - }; - - let active_cell = Some(Self::placeholder_session_header_cell(&config)); - - let current_cwd = Some(config.cwd.clone()); - let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); - let mut widget = Self { - app_event_tx: app_event_tx.clone(), - frame_requester: frame_requester.clone(), - codex_op_tx, - bottom_pane: BottomPane::new(BottomPaneParams { - frame_requester, - app_event_tx, - has_input_focus: true, - enhanced_keys_supported, - placeholder_text: placeholder, - disable_paste_burst: config.disable_paste_burst, - animations_enabled: config.animations, - skills: None, - }), - active_cell, - subagent_panel: None, - active_cell_revision: 0, - config, - skills_all: Vec::new(), - skills_initial_state: None, - current_collaboration_mode, - active_collaboration_mask, - auth_manager, - models_manager, - session_telemetry, - session_header: SessionHeader::new(header_model), - initial_user_message, - token_info: None, - rate_limit_snapshots_by_limit_id: BTreeMap::new(), - plan_type: None, - rate_limit_warnings: RateLimitWarningState::default(), - rate_limit_switch_prompt: RateLimitSwitchPromptState::default(), - rate_limit_poller: None, - adaptive_chunking: AdaptiveChunkingPolicy::default(), - stream_controller: None, - plan_stream_controller: None, - last_copyable_output: None, - running_commands: HashMap::new(), - pending_collab_spawn_requests: HashMap::new(), - suppressed_exec_calls: HashSet::new(), - last_unified_wait: None, - unified_exec_wait_streak: None, - turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep), - task_complete_pending: false, - unified_exec_processes: Vec::new(), - agent_turn_running: false, - mcp_startup_status: None, - connectors_cache: ConnectorsCacheState::default(), - connectors_partial_snapshot: None, - connectors_prefetch_in_flight: false, - connectors_force_refetch_pending: false, - pending_mcp_output_requests: 0, - plugins_cache: PluginsCacheState::default(), - plugins_fetch_state: PluginListFetchState::default(), - interrupts: InterruptManager::new(), - reasoning_buffer: String::new(), - full_reasoning_buffer: String::new(), - current_status: StatusIndicatorState::working(), - pending_guardian_review_status: PendingGuardianReviewStatus::default(), - terminal_title_status_kind: TerminalTitleStatusKind::Working, - retry_status_header: None, - pending_status_indicator_restore: false, - suppress_queue_autosend: false, - thread_id: None, - thread_name: None, - forked_from: None, - queued_user_messages: VecDeque::new(), - pending_steers: VecDeque::new(), - submit_pending_steers_after_interrupt: false, - queued_message_edit_binding, - show_welcome_banner: is_first_run, - startup_tooltip_override, - suppress_session_configured_redraw: false, - pending_notification: None, - quit_shortcut_expires_at: None, - quit_shortcut_key: None, - is_review_mode: false, - pre_review_token_info: None, - needs_final_message_separator: false, - had_work_activity: false, - saw_plan_update_this_turn: false, - saw_plan_item_this_turn: false, - last_plan_progress: None, - plan_delta_buffer: String::new(), - plan_item_active: false, - last_separator_elapsed_secs: None, - turn_runtime_metrics: RuntimeMetricsSummary::default(), - last_rendered_width: std::cell::Cell::new(None), - feedback, - feedback_audience, - current_rollout_path: None, - current_cwd, - session_network_proxy: None, - status_line_invalid_items_warned, - terminal_title_invalid_items_warned, - last_terminal_title: None, - terminal_title_setup_original_items: None, - terminal_title_animation_origin: Instant::now(), - status_line_project_root_name_cache: None, - status_line_branch: None, - status_line_branch_cwd: None, - status_line_branch_pending: false, - status_line_branch_lookup_complete: false, - external_editor_state: ExternalEditorState::Closed, - realtime_conversation: RealtimeConversationUiState::default(), - last_replayed_agent_inbox_message: None, - last_rendered_user_message_event: None, - }; - - widget.prefetch_rate_limits(); - widget.bottom_pane.set_voice_transcription_enabled( - widget.config.features.enabled(Feature::VoiceTranscription), - ); - widget - .bottom_pane - .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); - widget - .bottom_pane - .set_audio_device_selection_enabled(widget.realtime_audio_device_selection_enabled()); - widget - .bottom_pane - .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); - widget - .bottom_pane - .set_collaboration_modes_enabled(/*enabled*/ true); - widget.sync_fast_command_enabled(); - widget.sync_personality_command_enabled(); - widget.sync_plugins_command_enabled(); - widget - .bottom_pane - .set_queued_message_edit_binding(widget.queued_message_edit_binding); - #[cfg(target_os = "windows")] - widget.bottom_pane.set_windows_degraded_sandbox_active( - codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - ), - ); - widget.update_collaboration_mode_indicator(); - - widget - .bottom_pane - .set_connectors_enabled(widget.connectors_enabled()); - - widget.refresh_terminal_title(); - - widget } - #[allow(dead_code)] pub(crate) fn new_with_op_sender( common: ChatWidgetInit, @@ -4886,199 +4685,7 @@ impl ChatWidget { show_welcome_banner: is_first_run, startup_tooltip_override, suppress_session_configured_redraw: false, -<<<<<<< HEAD suppress_initial_user_message_submit: false, -======= - pending_notification: None, - quit_shortcut_expires_at: None, - quit_shortcut_key: None, - is_review_mode: false, - pre_review_token_info: None, - needs_final_message_separator: false, - had_work_activity: false, - last_separator_elapsed_secs: None, - turn_runtime_metrics: RuntimeMetricsSummary::default(), - last_rendered_width: std::cell::Cell::new(None), - feedback, - feedback_audience, - current_rollout_path: None, - current_cwd, - session_network_proxy: None, - status_line_invalid_items_warned, - terminal_title_invalid_items_warned, - last_terminal_title: None, - terminal_title_setup_original_items: None, - terminal_title_animation_origin: Instant::now(), - status_line_project_root_name_cache: None, - status_line_branch: None, - status_line_branch_cwd: None, - status_line_branch_pending: false, - status_line_branch_lookup_complete: false, - external_editor_state: ExternalEditorState::Closed, - realtime_conversation: RealtimeConversationUiState::default(), - last_replayed_agent_inbox_message: None, - last_rendered_user_message_event: None, - }; - - widget.prefetch_rate_limits(); - widget.bottom_pane.set_voice_transcription_enabled( - widget.config.features.enabled(Feature::VoiceTranscription), - ); - widget - .bottom_pane - .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); - widget - .bottom_pane - .set_audio_device_selection_enabled(widget.realtime_audio_device_selection_enabled()); - widget - .bottom_pane - .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); - widget - .bottom_pane - .set_collaboration_modes_enabled(/*enabled*/ true); - widget.sync_fast_command_enabled(); - widget.sync_personality_command_enabled(); - widget.sync_plugins_command_enabled(); - widget - .bottom_pane - .set_queued_message_edit_binding(widget.queued_message_edit_binding); - widget - .bottom_pane - .set_connectors_enabled(widget.connectors_enabled()); - widget.refresh_terminal_title(); - widget.refresh_terminal_title(); - - widget - } - - /// Create a ChatWidget attached to an existing conversation (e.g., a fork). - pub(crate) fn new_from_existing( - common: ChatWidgetInit, - conversation: std::sync::Arc, - session_configured: codex_protocol::protocol::SessionConfiguredEvent, - ) -> Self { - let ChatWidgetInit { - config, - frame_requester, - app_event_tx, - initial_user_message, - enhanced_keys_supported, - auth_manager, - models_manager, - feedback, - is_first_run: _, - feedback_audience, - model, - startup_tooltip_override: _, - status_line_invalid_items_warned, - terminal_title_invalid_items_warned, - session_telemetry, - } = common; - let model = model.filter(|m| !m.trim().is_empty()); - let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep); - let mut rng = rand::rng(); - let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string(); - - let model_override = model.as_deref(); - let header_model = model - .clone() - .unwrap_or_else(|| session_configured.model.clone()); - let active_collaboration_mask = - Self::initial_collaboration_mask(&config, models_manager.as_ref(), model_override); - let header_model = active_collaboration_mask - .as_ref() - .and_then(|mask| mask.model.clone()) - .unwrap_or(header_model); - - let current_cwd = Some(session_configured.cwd.clone()); - let codex_op_tx = - spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone()); - - let fallback_default = Settings { - model: header_model.clone(), - reasoning_effort: None, - developer_instructions: None, - }; - // Collaboration modes start in Default mode. - let current_collaboration_mode = CollaborationMode { - mode: ModeKind::Default, - settings: fallback_default, - }; - - let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); - let mut widget = Self { - app_event_tx: app_event_tx.clone(), - frame_requester: frame_requester.clone(), - codex_op_tx, - bottom_pane: BottomPane::new(BottomPaneParams { - frame_requester, - app_event_tx, - has_input_focus: true, - enhanced_keys_supported, - placeholder_text: placeholder, - disable_paste_burst: config.disable_paste_burst, - animations_enabled: config.animations, - skills: None, - }), - active_cell: None, - subagent_panel: None, - active_cell_revision: 0, - config, - skills_all: Vec::new(), - skills_initial_state: None, - current_collaboration_mode, - active_collaboration_mask, - auth_manager, - models_manager, - session_telemetry, - session_header: SessionHeader::new(header_model), - initial_user_message, - token_info: None, - rate_limit_snapshots_by_limit_id: BTreeMap::new(), - plan_type: None, - rate_limit_warnings: RateLimitWarningState::default(), - rate_limit_switch_prompt: RateLimitSwitchPromptState::default(), - rate_limit_poller: None, - adaptive_chunking: AdaptiveChunkingPolicy::default(), - stream_controller: None, - plan_stream_controller: None, - last_copyable_output: None, - running_commands: HashMap::new(), - pending_collab_spawn_requests: HashMap::new(), - suppressed_exec_calls: HashSet::new(), - last_unified_wait: None, - unified_exec_wait_streak: None, - turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep), - task_complete_pending: false, - unified_exec_processes: Vec::new(), - agent_turn_running: false, - mcp_startup_status: None, - connectors_cache: ConnectorsCacheState::default(), - connectors_partial_snapshot: None, - connectors_prefetch_in_flight: false, - connectors_force_refetch_pending: false, - pending_mcp_output_requests: 0, - plugins_cache: PluginsCacheState::default(), - plugins_fetch_state: PluginListFetchState::default(), - interrupts: InterruptManager::new(), - reasoning_buffer: String::new(), - full_reasoning_buffer: String::new(), - current_status: StatusIndicatorState::working(), - pending_guardian_review_status: PendingGuardianReviewStatus::default(), - terminal_title_status_kind: TerminalTitleStatusKind::Working, - retry_status_header: None, - pending_status_indicator_restore: false, - suppress_queue_autosend: false, - thread_id: None, - thread_name: None, - forked_from: None, - queued_user_messages: VecDeque::new(), - pending_steers: VecDeque::new(), - submit_pending_steers_after_interrupt: false, - queued_message_edit_binding, - show_welcome_banner: false, - startup_tooltip_override: None, - suppress_session_configured_redraw: true, pending_notification: None, quit_shortcut_expires_at: None, quit_shortcut_key: None, @@ -5117,6 +4724,10 @@ impl ChatWidget { last_non_retry_error: None, }; + widget.prefetch_rate_limits(); + widget.bottom_pane.set_voice_transcription_enabled( + widget.config.features.enabled(Feature::VoiceTranscription), + ); widget .bottom_pane .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); @@ -5135,20 +4746,11 @@ impl ChatWidget { widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_binding); - #[cfg(target_os = "windows")] - widget.bottom_pane.set_windows_degraded_sandbox_active( - codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - ), - ); - widget.update_collaboration_mode_indicator(); - widget .bottom_pane .set_connectors_enabled(widget.connectors_enabled()); - widget.refresh_status_surfaces(); + widget.refresh_terminal_title(); + widget.refresh_terminal_title(); widget } @@ -11355,7 +10957,7 @@ const PLACEHOLDERS: [&str; 8] = [ // Extract the first bold (Markdown) element in the form **...** from `s`. // Returns the inner text if found; otherwise `None`. -fn extract_first_bold(s: &str) -> Option { +pub(crate) fn extract_first_bold(s: &str) -> Option { let bytes = s.as_bytes(); let mut i = 0usize; while i + 1 < bytes.len() { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap.new b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap.new deleted file mode 100644 index 7ac97d97c5..0000000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap.new +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -assertion_line: 9607 -expression: popup ---- - Update Model Permissions - -› 1. Default Codex can read and edit files in the current - workspace, and run commands. Approval is required to - access the internet or edit other files. - 2. Guardian Approvals Same workspace-write permissions as Default, but - eligible `on-request` approvals are routed through - the guardian reviewer subagent. - 3. Full Access Codex can edit files outside this workspace and - access the internet without asking for approval. - Exercise caution when using. - - Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__subagent_panel_mounts_while_placeholder_active_cell_exists.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__subagent_panel_mounts_while_placeholder_active_cell_exists.snap index d285b60137..895663fc41 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__subagent_panel_mounts_while_placeholder_active_cell_exists.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__subagent_panel_mounts_while_placeholder_active_cell_exists.snap @@ -2,12 +2,12 @@ source: tui/src/chatwidget/tests.rs expression: terminal.backend().vt100().screen().contents() --- -╭──────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ model: loading /model to change │ -│ directory: ~/code/codex-split/…/codex-rs/tui │ -╰──────────────────────────────────────────────╯ +╭───────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ model: loading /model to change │ +│ directory: /tmp/project │ +╰───────────────────────────────────────╯ • Subagents (0s • no subagents running • esc to interrupt) • [#1] [watchdog] watchdog-agent idle — watchdog idle @@ -15,4 +15,4 @@ expression: terminal.backend().vt100().screen().contents() › show current subagent state - 100% context left + gpt-5.3-codex default · 100% left · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__user_prompt_submit_app_server_hook_notifications_render_snapshot.snap.new b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__user_prompt_submit_app_server_hook_notifications_render_snapshot.snap.new deleted file mode 100644 index c7e2394230..0000000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__user_prompt_submit_app_server_hook_notifications_render_snapshot.snap.new +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -assertion_line: 12789 -expression: combined ---- -• Running UserPromptSubmit hook: checking go-workflow input policy - -UserPromptSubmit hook (stopped) - warning: go-workflow must start from PlanMode - stop: prompt blocked