Show session header before configuration (#9568)

We were skipping if we know the model. We shouldn't
This commit is contained in:
Ahmed Ibrahim 2026-01-20 18:13:54 -08:00 committed by GitHub
parent ac2090caf2
commit 3a0eeb8edf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 183 additions and 44 deletions

View file

@ -62,7 +62,9 @@ async fn thread_resume_returns_original_thread() -> Result<()> {
let ThreadResumeResponse {
thread: resumed, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resumed, thread);
let mut expected = thread;
expected.updated_at = resumed.updated_at;
assert_eq!(resumed, expected);
Ok(())
}
@ -179,7 +181,9 @@ async fn thread_resume_prefers_path_over_thread_id() -> Result<()> {
let ThreadResumeResponse {
thread: resumed, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resumed, thread);
let mut expected = thread;
expected.updated_at = resumed.updated_at;
assert_eq!(resumed, expected);
Ok(())
}

View file

@ -1807,9 +1807,7 @@ impl ChatWidget {
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_for_header = model
.clone()
.unwrap_or_else(|| DEFAULT_MODEL_DISPLAY_NAME.to_string());
let model_for_header = model.unwrap_or_else(|| DEFAULT_MODEL_DISPLAY_NAME.to_string());
let stored_collaboration_mode = if config.features.enabled(Feature::CollaborationModes) {
collaboration_modes::default_mode(models_manager.as_ref()).unwrap_or_else(|| {
CollaborationMode::Custom(Settings {
@ -1826,15 +1824,11 @@ impl ChatWidget {
})
};
let active_cell = if model.is_none() {
Some(Self::placeholder_session_header_cell(
&config,
config.features.enabled(Feature::CollaborationModes),
stored_collaboration_mode.clone(),
))
} else {
None
};
let active_cell = Some(Self::placeholder_session_header_cell(
&config,
config.features.enabled(Feature::CollaborationModes),
stored_collaboration_mode.clone(),
));
let mut widget = Self {
app_event_tx: app_event_tx.clone(),

View file

@ -1123,7 +1123,9 @@ impl HistoryCell for SessionHeaderHistoryCell {
label_width = label_width
);
let mut spans = vec![Span::from(format!("{collab_label} ")).dim()];
if let Some(mode_label) = self.collaboration_mode_label() {
if self.model == "loading" {
spans.push(Span::styled(self.model.clone(), self.model_style));
} else if let Some(mode_label) = self.collaboration_mode_label() {
spans.push(Span::styled(mode_label.to_string(), self.model_style));
} else {
spans.push(Span::styled("Custom", self.model_style));

View file

@ -2719,6 +2719,14 @@ mod tests {
app.chat_widget.current_model(),
event,
is_first,
false,
codex_protocol::config_types::CollaborationMode::Custom(
codex_protocol::config_types::Settings {
model: "gpt-test".to_string(),
reasoning_effort: None,
developer_instructions: None,
},
),
)) as Arc<dyn HistoryCell>
};

View file

@ -162,6 +162,7 @@ use crate::slash_command::SlashCommand;
use crate::status::RateLimitSnapshotDisplay;
use crate::text_formatting::truncate_text;
use crate::tui::FrameRequester;
use crate::ui_consts::DEFAULT_MODEL_DISPLAY_NAME;
mod interrupts;
use self::interrupts::InterruptManager;
mod agent;
@ -213,7 +214,6 @@ impl UnifiedExecWaitState {
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
const NUDGE_MODEL_SLUG: &str = "gpt-5.1-codex-mini";
const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0;
const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
#[derive(Default)]
struct RateLimitWarningState {
@ -650,6 +650,8 @@ impl ChatWidget {
&model_for_header,
event,
self.show_welcome_banner,
self.collaboration_modes_enabled(),
self.stored_collaboration_mode.clone(),
);
self.apply_session_info_cell(session_info_cell);
@ -1610,15 +1612,7 @@ impl ChatWidget {
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_for_header = model
.clone()
.unwrap_or_else(|| DEFAULT_MODEL_DISPLAY_NAME.to_string());
let active_cell = if model.is_none() {
Some(Self::placeholder_session_header_cell(&config))
} else {
None
};
let model_for_header = model.unwrap_or_else(|| DEFAULT_MODEL_DISPLAY_NAME.to_string());
let stored_collaboration_mode = if config.features.enabled(Feature::CollaborationModes) {
collaboration_modes::default_mode(models_manager.as_ref()).unwrap_or_else(|| {
CollaborationMode::Custom(Settings {
@ -1634,6 +1628,11 @@ impl ChatWidget {
developer_instructions: None,
})
};
let active_cell = Some(Self::placeholder_session_header_cell(
&config,
config.features.enabled(Feature::CollaborationModes),
stored_collaboration_mode.clone(),
));
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
@ -4051,7 +4050,11 @@ impl ChatWidget {
}
/// Build a placeholder header cell while the session is configuring.
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
fn placeholder_session_header_cell(
config: &Config,
is_collaboration: bool,
collaboration_mode: CollaborationMode,
) -> Box<dyn HistoryCell> {
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
Box::new(history_cell::SessionHeaderHistoryCell::new_with_style(
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
@ -4059,6 +4062,8 @@ impl ChatWidget {
None,
config.cwd.clone(),
CODEX_CLI_VERSION,
is_collaboration,
collaboration_mode,
))
}

View file

@ -19,6 +19,7 @@ use crate::exec_cell::output_lines;
use crate::exec_cell::spinner;
use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::key_hint;
use crate::markdown::append_markdown;
use crate::render::line_utils::line_to_static;
use crate::render::line_utils::prefix_lines;
@ -27,6 +28,7 @@ use crate::style::user_message_style;
use crate::text_formatting::format_and_truncate_tool_result;
use crate::text_formatting::truncate_text;
use crate::tooltips;
use crate::ui_consts::DEFAULT_MODEL_DISPLAY_NAME;
use crate::ui_consts::LIVE_PREFIX_COLS;
use crate::update_action::UpdateAction;
use crate::version::CODEX_CLI_VERSION;
@ -40,11 +42,13 @@ use codex_core::protocol::FileChange;
use codex_core::protocol::McpAuthStatus;
use codex_core::protocol::McpInvocation;
use codex_core::protocol::SessionConfiguredEvent;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::plan_tool::PlanItemArg;
use codex_protocol::plan_tool::StepStatus;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::user_input::TextElement;
use crossterm::event::KeyCode;
use image::DynamicImage;
use image::ImageReader;
use mcp_types::EmbeddedResourceResource;
@ -971,6 +975,8 @@ pub(crate) fn new_session_info(
requested_model: &str,
event: SessionConfiguredEvent,
is_first_event: bool,
is_collaboration: bool,
collaboration_mode: CollaborationMode,
) -> SessionInfoCell {
let SessionConfiguredEvent {
model,
@ -984,6 +990,8 @@ pub(crate) fn new_session_info(
reasoning_effort,
config.cwd.clone(),
CODEX_CLI_VERSION,
is_collaboration,
collaboration_mode,
);
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
@ -1060,6 +1068,8 @@ pub(crate) struct SessionHeaderHistoryCell {
model_style: Style,
reasoning_effort: Option<ReasoningEffortConfig>,
directory: PathBuf,
is_collaboration: bool,
collaboration_mode: CollaborationMode,
}
impl SessionHeaderHistoryCell {
@ -1069,8 +1079,18 @@ impl SessionHeaderHistoryCell {
reasoning_effort: Option<ReasoningEffortConfig>,
directory: PathBuf,
version: &'static str,
is_collaboration: bool,
collaboration_mode: CollaborationMode,
) -> Self {
Self::new_with_style(model, model_style, reasoning_effort, directory, version)
Self::new_with_style(
model,
model_style,
reasoning_effort,
directory,
version,
is_collaboration,
collaboration_mode,
)
}
pub(crate) fn new_with_style(
@ -1079,6 +1099,8 @@ impl SessionHeaderHistoryCell {
reasoning_effort: Option<ReasoningEffortConfig>,
directory: PathBuf,
version: &'static str,
is_collaboration: bool,
collaboration_mode: CollaborationMode,
) -> Self {
Self {
version,
@ -1086,6 +1108,20 @@ impl SessionHeaderHistoryCell {
model_style,
reasoning_effort,
directory,
is_collaboration,
collaboration_mode,
}
}
fn collaboration_mode_label(&self) -> Option<&'static str> {
if !self.is_collaboration {
return None;
}
match &self.collaboration_mode {
CollaborationMode::Plan(_) => Some("Plan"),
CollaborationMode::PairProgramming(_) => Some("Pair Programming"),
CollaborationMode::Execute(_) => Some("Execute"),
CollaborationMode::Custom(_) => None,
}
}
@ -1146,25 +1182,48 @@ impl HistoryCell for SessionHeaderHistoryCell {
const CHANGE_MODEL_HINT_COMMAND: &str = "/model";
const CHANGE_MODEL_HINT_EXPLANATION: &str = " to change";
const CHANGE_MODE_HINT_EXPLANATION: &str = " to change mode";
const DIR_LABEL: &str = "directory:";
let label_width = DIR_LABEL.len();
let model_label = format!(
"{model_label:<label_width$}",
model_label = "model:",
label_width = label_width
);
let reasoning_label = self.reasoning_label();
let mut model_spans: Vec<Span<'static>> = vec![
Span::from(format!("{model_label} ")).dim(),
Span::styled(self.model.clone(), self.model_style),
];
if let Some(reasoning) = reasoning_label {
model_spans.push(Span::from(" "));
model_spans.push(Span::from(reasoning));
}
model_spans.push(" ".dim());
model_spans.push(CHANGE_MODEL_HINT_COMMAND.cyan());
model_spans.push(CHANGE_MODEL_HINT_EXPLANATION.dim());
let model_spans: Vec<Span<'static>> = if self.is_collaboration {
let collab_label = format!(
"{collab_label:<label_width$}",
collab_label = "mode:",
label_width = label_width
);
let mut spans = vec![Span::from(format!("{collab_label} ")).dim()];
if self.model == DEFAULT_MODEL_DISPLAY_NAME {
spans.push(Span::styled(self.model.clone(), self.model_style));
} else if let Some(mode_label) = self.collaboration_mode_label() {
spans.push(Span::styled(mode_label.to_string(), self.model_style));
} else {
spans.push(Span::styled("Custom", self.model_style));
}
spans.push(" ".dim());
let shift_tab_span: Span<'static> = key_hint::shift(KeyCode::Tab).into();
spans.push(shift_tab_span.cyan());
spans.push(CHANGE_MODE_HINT_EXPLANATION.dim());
spans
} else {
let model_label = format!(
"{model_label:<label_width$}",
model_label = "model:",
label_width = label_width
);
let reasoning_label = self.reasoning_label();
let mut spans = vec![
Span::from(format!("{model_label} ")).dim(),
Span::styled(self.model.clone(), self.model_style),
];
if let Some(reasoning) = reasoning_label {
spans.push(Span::from(" "));
spans.push(Span::from(reasoning));
}
spans.push(" ".dim());
spans.push(CHANGE_MODEL_HINT_COMMAND.cyan());
spans.push(CHANGE_MODEL_HINT_EXPLANATION.dim());
spans
};
let dir_label = format!("{DIR_LABEL:<label_width$}");
let dir_prefix = format!("{dir_label} ");
@ -1893,11 +1952,14 @@ mod tests {
use crate::exec_cell::CommandOutput;
use crate::exec_cell::ExecCall;
use crate::exec_cell::ExecCell;
use crate::ui_consts::DEFAULT_MODEL_DISPLAY_NAME;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::types::McpServerConfig;
use codex_core::config::types::McpServerTransportConfig;
use codex_core::protocol::McpAuthStatus;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Settings;
use codex_protocol::parse_command::ParsedCommand;
use dirs::home_dir;
use pretty_assertions::assert_eq;
@ -1919,6 +1981,22 @@ mod tests {
.expect("config")
}
fn default_collaboration_mode(model: &str) -> CollaborationMode {
CollaborationMode::Custom(Settings {
model: model.to_string(),
reasoning_effort: None,
developer_instructions: None,
})
}
fn plan_collaboration_mode(model: &str) -> CollaborationMode {
CollaborationMode::Plan(Settings {
model: model.to_string(),
reasoning_effort: None,
developer_instructions: None,
})
}
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
lines
.iter()
@ -2420,6 +2498,8 @@ mod tests {
Some(ReasoningEffortConfig::High),
std::env::temp_dir(),
"test",
false,
default_collaboration_mode("gpt-4o"),
);
let lines = render_lines(&cell.display_lines(80));
@ -2432,6 +2512,51 @@ mod tests {
assert!(model_line.contains("/model to change"));
}
#[test]
fn session_header_collaboration_mode_includes_label_and_hint() {
let cell = SessionHeaderHistoryCell::new(
"gpt-4o".to_string(),
Style::default(),
None,
std::env::temp_dir(),
"test",
true,
plan_collaboration_mode("gpt-4o"),
);
let lines = render_lines(&cell.display_lines(80));
let mode_line = lines
.into_iter()
.find(|line| line.contains("mode:"))
.expect("mode line");
assert!(mode_line.contains("Plan"));
assert!(mode_line.contains("shift + tab"));
assert!(mode_line.contains("to change mode"));
}
#[test]
fn session_header_collaboration_mode_uses_placeholder_when_loading() {
let cell = SessionHeaderHistoryCell::new(
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
Style::default(),
None,
std::env::temp_dir(),
"test",
true,
plan_collaboration_mode("gpt-4o"),
);
let lines = render_lines(&cell.display_lines(80));
let mode_line = lines
.into_iter()
.find(|line| line.contains("mode:"))
.expect("mode line");
assert!(mode_line.contains(DEFAULT_MODEL_DISPLAY_NAME));
assert!(!mode_line.contains("Plan"));
}
#[test]
fn session_header_directory_center_truncates() {
let mut dir = home_dir().expect("home directory");

View file

@ -9,3 +9,4 @@
/// - User history lines account for this many columns (e.g., "▌ ") when wrapping.
pub(crate) const LIVE_PREFIX_COLS: u16 = 2;
pub(crate) const FOOTER_INDENT_COLS: usize = LIVE_PREFIX_COLS as usize;
pub(crate) const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";