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
73 changes: 70 additions & 3 deletions codex-rs/core/src/models_manager/collaboration_mode_presets.rs
Original file line number Diff line number Diff line change
@@ -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()]
Expand All @@ -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)),
Expand All @@ -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));
}
}
52 changes: 12 additions & 40 deletions codex-rs/core/src/tools/handlers/request_user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,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(),
Expand All @@ -38,15 +26,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"
))
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/templates/collaboration_mode/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
34 changes: 34 additions & 0 deletions codex-rs/protocol/src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,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")]
Expand Down Expand Up @@ -323,4 +344,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());
}
}
9 changes: 4 additions & 5 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5473,11 +5473,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> {
Expand Down
8 changes: 2 additions & 6 deletions codex-rs/tui/src/collaboration_modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand All @@ -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)
Expand Down
Loading