fix(tui): restore subagent panel wiring on app-server TUI

This commit is contained in:
Friel
2026-03-28 18:28:25 -07:00
parent 90d339cf75
commit fc7a5e3d0f
6 changed files with 263 additions and 441 deletions
+241
View File
@@ -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<AtomicBool>,
/// Controls the animation thread that updates the live subagent panel.
pub(crate) subagent_anim_running: Arc<AtomicBool>,
// Shared across ChatWidget instances so invalid status-line config warnings only emit once.
status_line_invalid_items_warned: Arc<AtomicBool>,
// 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<ThreadId, ThreadEventChannel>,
thread_event_listener_tasks: HashMap<ThreadId, JoinHandle<()>>,
subagents: SubagentRegistry,
agent_navigation: AgentNavigationState,
active_thread_id: Option<ThreadId>,
active_thread_rx: Option<mpsc::Receiver<ThreadBufferedEvent>>,
@@ -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<dyn HistoryCell>) {
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) => &notification.item,
ServerNotification::ItemCompleted(notification) => &notification.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,
+7
View File
@@ -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<SubagentStatusCell>),
ClearSubagentPanel,
/// Update the current reasoning effort in the running app and widget.
UpdateReasoningEffort(Option<ReasoningEffort>),
+8 -406
View File
@@ -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<ThreadManager>) -> 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<codex_core::CodexThread>,
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<String> {
pub(crate) fn extract_first_bold(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut i = 0usize;
while i + 1 < bytes.len() {
@@ -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
@@ -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
@@ -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