Sync collaboration mode naming across Default prompt, tools, and TUI (#10666)

## Summary
- add shared `ModeKind` helpers for display names, TUI visibility, and
`request_user_input` availability
- derive TUI mode filtering/labels from shared `ModeKind` metadata
instead of local hardcoded matches
- derive `request_user_input` availability text and unavailable error
mode names from shared mode metadata
- replace hardcoded known mode names in the Default collaboration-mode
template with `{{KNOWN_MODE_NAMES}}` and fill it from
`TUI_VISIBLE_COLLABORATION_MODES`
- add regression tests for mode metadata sync and placeholder
replacement

## Notes
- `cargo test -p codex-core` integration target (`tests/all`) still
shows pre-existing env-specific failures in this environment due missing
`test_stdio_server` binary resolution; core unit tests are green.

## Codex author
`codex resume 019c26ff-dfe7-7173-bc04-c9e1fff1e447`
This commit is contained in:
Charley Cunningham 2026-02-04 23:03:28 -08:00 committed by GitHub
parent e482978261
commit 41b4962b0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 125 additions and 55 deletions

View file

@ -1,10 +1,13 @@
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
use codex_protocol::openai_models::ReasoningEffort;
const COLLABORATION_MODE_PLAN: &str = include_str!("../../templates/collaboration_mode/plan.md");
const COLLABORATION_MODE_DEFAULT: &str =
include_str!("../../templates/collaboration_mode/default.md");
const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}";
const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}";
pub(super) fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
vec![plan_preset(), default_preset()]
@ -17,7 +20,7 @@ pub fn test_builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
fn plan_preset() -> CollaborationModeMask {
CollaborationModeMask {
name: "Plan".to_string(),
name: ModeKind::Plan.display_name().to_string(),
mode: Some(ModeKind::Plan),
model: None,
reasoning_effort: Some(Some(ReasoningEffort::Medium)),
@ -27,10 +30,74 @@ fn plan_preset() -> CollaborationModeMask {
fn default_preset() -> CollaborationModeMask {
CollaborationModeMask {
name: "Default".to_string(),
name: ModeKind::Default.display_name().to_string(),
mode: Some(ModeKind::Default),
model: None,
reasoning_effort: None,
developer_instructions: Some(Some(COLLABORATION_MODE_DEFAULT.to_string())),
developer_instructions: Some(Some(default_mode_instructions())),
}
}
fn default_mode_instructions() -> String {
let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES);
let request_user_input_availability =
request_user_input_availability_message(ModeKind::Default);
COLLABORATION_MODE_DEFAULT
.replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names)
.replace(
REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER,
&request_user_input_availability,
)
}
fn format_mode_names(modes: &[ModeKind]) -> String {
let mode_names: Vec<&str> = modes.iter().map(|mode| mode.display_name()).collect();
match mode_names.as_slice() {
[] => "none".to_string(),
[mode_name] => (*mode_name).to_string(),
[first, second] => format!("{first} and {second}"),
[..] => mode_names.join(", "),
}
}
fn request_user_input_availability_message(mode: ModeKind) -> String {
let mode_name = mode.display_name();
if mode.allows_request_user_input() {
format!("The `request_user_input` tool is available in {mode_name} mode.")
} else {
format!(
"The `request_user_input` tool is unavailable in {mode_name} mode. If you call it while in {mode_name} mode, it will return an error."
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn preset_names_use_mode_display_names() {
assert_eq!(plan_preset().name, ModeKind::Plan.display_name());
assert_eq!(default_preset().name, ModeKind::Default.display_name());
}
#[test]
fn default_mode_instructions_replace_mode_names_placeholder() {
let default_instructions = default_preset()
.developer_instructions
.expect("default preset should include instructions")
.expect("default instructions should be set");
assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER));
assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER));
let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES);
let expected_snippet = format!("Known mode names are {known_mode_names}.");
assert!(default_instructions.contains(&expected_snippet));
let expected_availability_message =
request_user_input_availability_message(ModeKind::Default);
assert!(default_instructions.contains(&expected_availability_message));
}
}

View file

@ -9,27 +9,15 @@ use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
use codex_protocol::request_user_input::RequestUserInputArgs;
const REQUEST_USER_INPUT_ALLOWED_MODES: [ModeKind; 1] = [ModeKind::Plan];
fn request_user_input_mode_name(mode: ModeKind) -> &'static str {
match mode {
ModeKind::Plan => "Plan",
ModeKind::Default => "Default",
ModeKind::Execute => "Execute",
ModeKind::PairProgramming => "Pair Programming",
}
}
fn format_allowed_modes() -> String {
let mut mode_names = Vec::with_capacity(REQUEST_USER_INPUT_ALLOWED_MODES.len());
for mode in REQUEST_USER_INPUT_ALLOWED_MODES {
let name = request_user_input_mode_name(mode);
if !mode_names.contains(&name) {
mode_names.push(name);
}
}
let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES
.into_iter()
.filter(|mode| mode.allows_request_user_input())
.map(ModeKind::display_name)
.collect();
match mode_names.as_slice() {
[] => "no modes".to_string(),
@ -39,15 +27,11 @@ fn format_allowed_modes() -> String {
}
}
fn request_user_input_is_available_in_mode(mode: ModeKind) -> bool {
REQUEST_USER_INPUT_ALLOWED_MODES.contains(&mode)
}
pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option<String> {
if request_user_input_is_available_in_mode(mode) {
if mode.allows_request_user_input() {
None
} else {
let mode_name = request_user_input_mode_name(mode);
let mode_name = mode.display_name();
Some(format!(
"request_user_input is unavailable in {mode_name} mode"
))
@ -134,22 +118,10 @@ mod tests {
#[test]
fn request_user_input_mode_availability_is_plan_only() {
assert_eq!(
request_user_input_is_available_in_mode(ModeKind::Plan),
true
);
assert_eq!(
request_user_input_is_available_in_mode(ModeKind::Default),
false
);
assert_eq!(
request_user_input_is_available_in_mode(ModeKind::Execute),
false
);
assert_eq!(
request_user_input_is_available_in_mode(ModeKind::PairProgramming),
false
);
assert!(ModeKind::Plan.allows_request_user_input());
assert!(!ModeKind::Default.allows_request_user_input());
assert!(!ModeKind::Execute.allows_request_user_input());
assert!(!ModeKind::PairProgramming.allows_request_user_input());
}
#[test]

View file

@ -2,8 +2,10 @@
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are {{KNOWN_MODE_NAMES}}.
## request_user_input availability
The `request_user_input` tool is unavailable in Default mode. If you call it while in Default mode, it will return an error.
{{REQUEST_USER_INPUT_AVAILABILITY}}
If a decision is necessary and cannot be discovered from local context, ask the user directly. However, in Default mode you should strongly prefer executing the user's request rather than stopping to ask questions.

View file

@ -193,6 +193,27 @@ pub enum ModeKind {
Execute,
}
pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];
impl ModeKind {
pub const fn display_name(self) -> &'static str {
match self {
Self::Plan => "Plan",
Self::Default => "Default",
Self::PairProgramming => "Pair Programming",
Self::Execute => "Execute",
}
}
pub const fn is_tui_visible(self) -> bool {
matches!(self, Self::Plan | Self::Default)
}
pub const fn allows_request_user_input(self) -> bool {
matches!(self, Self::Plan)
}
}
/// Collaboration mode for a Codex session.
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
@ -324,4 +345,17 @@ mod tests {
assert_eq!(ModeKind::Default, mode);
}
}
#[test]
fn tui_visible_collaboration_modes_match_mode_kind_visibility() {
let expected = [ModeKind::Default, ModeKind::Plan];
assert_eq!(expected, TUI_VISIBLE_COLLABORATION_MODES);
for mode in TUI_VISIBLE_COLLABORATION_MODES {
assert!(mode.is_tui_visible());
}
assert!(!ModeKind::PairProgramming.is_tui_visible());
assert!(!ModeKind::Execute.is_tui_visible());
}
}

View file

@ -5490,11 +5490,10 @@ impl ChatWidget {
if !self.collaboration_modes_enabled() {
return None;
}
match self.active_mode_kind() {
ModeKind::Plan => Some("Plan"),
ModeKind::Default => Some("Default"),
ModeKind::PairProgramming | ModeKind::Execute => None,
}
let active_mode = self.active_mode_kind();
active_mode
.is_tui_visible()
.then_some(active_mode.display_name())
}
fn collaboration_mode_indicator(&self) -> Option<CollaborationModeIndicator> {

View file

@ -2,15 +2,11 @@ use codex_core::models_manager::manager::ModelsManager;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ModeKind;
fn is_tui_mode(kind: ModeKind) -> bool {
matches!(kind, ModeKind::Plan | ModeKind::Default)
}
fn filtered_presets(models_manager: &ModelsManager) -> Vec<CollaborationModeMask> {
models_manager
.list_collaboration_modes()
.into_iter()
.filter(|mask| mask.mode.is_some_and(is_tui_mode))
.filter(|mask| mask.mode.is_some_and(ModeKind::is_tui_visible))
.collect()
}
@ -31,7 +27,7 @@ pub(crate) fn mask_for_kind(
models_manager: &ModelsManager,
kind: ModeKind,
) -> Option<CollaborationModeMask> {
if !is_tui_mode(kind) {
if !kind.is_tui_visible() {
return None;
}
filtered_presets(models_manager)