Cleanup collaboration mode variants (#10404)

## Summary

This PR simplifies collaboration modes to the visible set `default |
plan`, while preserving backward compatibility for older partners that
may still send legacy mode
names.

Specifically:
- Renames the old Code behavior to **Default**.
- Keeps **Plan** as-is.
- Removes **Custom** mode behavior (fallbacks now resolve to Default).
- Keeps `PairProgramming` and `Execute` internally for compatibility
plumbing, while removing them from schema/API and UI visibility.
- Adds legacy input aliasing so older clients can still send old mode
names.

## What Changed

1. Mode enum and compatibility
- `ModeKind` now uses `Plan` + `Default` as active/public modes.
- `ModeKind::Default` deserialization accepts legacy values:
  - `code`
  - `pair_programming`
  - `execute`
  - `custom`
- `PairProgramming` and `Execute` variants remain in code but are hidden
from protocol/schema generation.
- `Custom` variant is removed; previous custom fallbacks now map to
`Default`.

2. Collaboration presets and templates
- Built-in presets now return only:
  - `Plan`
  - `Default`
- Template rename:
  - `core/templates/collaboration_mode/code.md` -> `default.md`
- `execute.md` and `pair_programming.md` remain on disk but are not
surfaced in visible preset lists.

3. TUI updates
- Updated user-facing naming and prompts from “Code” to “Default”.
- Updated mode-cycle and indicator behavior to reflect only visible
`Plan` and `Default`.
- Updated corresponding tests and snapshots.

4. request_user_input behavior
- `request_user_input` remains allowed only in `Plan` mode.
- Rejection messaging now consistently treats non-plan modes as
`Default`.

5. Schemas
- Regenerated config and app-server schemas.
- Public schema types now advertise mode values as:
  - `plan`
  - `default`

## Backward Compatibility Notes

- Incoming legacy mode names (`code`, `pair_programming`, `execute`,
`custom`) are accepted and coerced to `default`.
- Outgoing/public schema surfaces intentionally expose only `plan |
default`.
- This allows tolerant ingestion of older partner payloads while
standardizing new integrations on the reduced mode set.

## Codex author
`codex fork 019c1fae-693b-7840-b16e-9ad38ea0bd00`
This commit is contained in:
Charley Cunningham
2026-02-03 09:23:53 -08:00
committed by GitHub
Unverified
parent aea38f0f88
commit d509df676b
34 changed files with 205 additions and 250 deletions
@@ -1049,10 +1049,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3122,10 +3122,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -5078,7 +5075,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -1129,7 +1129,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3901,10 +3901,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -2391,7 +2391,7 @@
"$ref": "#/definitions/v2/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -5850,10 +5850,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -11778,10 +11775,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3122,10 +3122,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3122,10 +3122,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -551,7 +551,7 @@
"$ref": "#/definitions/ModeKind"
}
],
"default": "code"
"default": "default"
},
"model_context_window": {
"format": "int64",
@@ -3122,10 +3122,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -53,10 +53,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -5,4 +5,4 @@
/**
* Initial collaboration mode to use when the TUI starts.
*/
export type ModeKind = "plan" | "code" | "pair_programming" | "execute" | "custom";
export type ModeKind = "plan" | "default";
@@ -1,8 +1,8 @@
//! Validates that the collaboration mode list endpoint returns the expected default presets.
//!
//! The test drives the app server through the MCP harness and asserts that the list response
//! includes the plan, coding, pair programming, and execute modes with their default model and reasoning
//! effort settings, which keeps the API contract visible in one place.
//! includes the plan and default modes with their default model and reasoning effort
//! settings, which keeps the API contract visible in one place.
#![allow(clippy::unwrap_used)]
@@ -45,23 +45,8 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> {
let CollaborationModeListResponse { data: items } =
to_response::<CollaborationModeListResponse>(response)?;
let expected = [
plan_preset(),
code_preset(),
pair_programming_preset(),
execute_preset(),
];
assert_eq!(expected.len(), items.len());
for (expected_mask, actual_mask) in expected.iter().zip(items.iter()) {
assert_eq!(expected_mask.name, actual_mask.name);
assert_eq!(expected_mask.mode, actual_mask.mode);
assert_eq!(expected_mask.model, actual_mask.model);
assert_eq!(expected_mask.reasoning_effort, actual_mask.reasoning_effort);
assert_eq!(
expected_mask.developer_instructions,
actual_mask.developer_instructions
);
}
let expected = vec![plan_preset(), default_preset()];
assert_eq!(expected, items);
Ok(())
}
@@ -77,35 +62,11 @@ fn plan_preset() -> CollaborationModeMask {
.unwrap()
}
/// Builds the pair programming preset that the list response is expected to return.
///
/// The helper keeps the expected model and reasoning defaults co-located with the test
/// so that mismatches point directly at the API contract being exercised.
fn pair_programming_preset() -> CollaborationModeMask {
/// Builds the default preset that the list response is expected to return.
fn default_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::PairProgramming))
.unwrap()
}
/// Builds the code preset that the list response is expected to return.
fn code_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Code))
.unwrap()
}
/// Builds the execute preset that the list response is expected to return.
///
/// The execute preset uses a different reasoning effort to capture the higher-effort
/// execution contract the server currently exposes.
fn execute_preset() -> CollaborationModeMask {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == Some(ModeKind::Execute))
.find(|p| p.mode == Some(ModeKind::Default))
.unwrap()
}
@@ -365,7 +365,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: "mock-model-collab".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
+2 -5
View File
@@ -378,10 +378,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"
"default"
],
"type": "string"
},
@@ -1021,7 +1018,7 @@
}
],
"default": null,
"description": "Start the TUI in the specified collaboration mode (plan/execute/etc.). Defaults to unset."
"description": "Start the TUI in the specified collaboration mode (plan/default). Defaults to unset."
},
"notification_method": {
"allOf": [
+1 -1
View File
@@ -232,7 +232,7 @@ mod tests {
async fn on_event_updates_status_from_task_started() {
let status = agent_status_from_event(&EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}));
assert_eq!(status, Some(AgentStatus::Running));
}
+6 -6
View File
@@ -341,7 +341,7 @@ impl Codex {
// TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
// to avoid extracting these fields separately and constructing CollaborationMode here.
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: model.clone(),
reasoning_effort: config.model_reasoning_effort,
@@ -2603,7 +2603,7 @@ mod handlers {
} => {
let collaboration_mode = collaboration_mode.or_else(|| {
Some(CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: model.clone(),
reasoning_effort: effort,
@@ -4942,7 +4942,7 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort,
@@ -5025,7 +5025,7 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort,
@@ -5295,7 +5295,7 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort,
@@ -5415,7 +5415,7 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config);
let reasoning_effort = config.model_reasoning_effort;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort,
+1 -1
View File
@@ -213,7 +213,7 @@ pub struct Config {
/// Show startup tooltips in the TUI welcome screen.
pub show_tooltips: bool,
/// Start the TUI in the specified collaboration mode (plan/execute/etc.).
/// Start the TUI in the specified collaboration mode (plan/default).
pub experimental_mode: Option<ModeKind>,
/// Controls whether the TUI uses the terminal's alternate screen buffer.
+1 -1
View File
@@ -471,7 +471,7 @@ pub struct Tui {
#[serde(default = "default_true")]
pub show_tooltips: bool,
/// Start the TUI in the specified collaboration mode (plan/execute/etc.).
/// Start the TUI in the specified collaboration mode (plan/default).
/// Defaults to unset.
#[serde(default)]
pub experimental_mode: Option<ModeKind>,
+1 -1
View File
@@ -121,7 +121,7 @@ pub enum Feature {
SkillEnvVarDependencyPrompt,
/// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
Steer,
/// Enable collaboration modes (Plan, Code, Pair Programming, Execute).
/// Enable collaboration modes (Plan, Default).
CollaborationModes,
/// Enable personality selection in the TUI.
Personality,
@@ -3,19 +3,11 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ReasoningEffort;
const COLLABORATION_MODE_PLAN: &str = include_str!("../../templates/collaboration_mode/plan.md");
const COLLABORATION_MODE_CODE: &str = include_str!("../../templates/collaboration_mode/code.md");
const COLLABORATION_MODE_PAIR_PROGRAMMING: &str =
include_str!("../../templates/collaboration_mode/pair_programming.md");
const COLLABORATION_MODE_EXECUTE: &str =
include_str!("../../templates/collaboration_mode/execute.md");
const COLLABORATION_MODE_DEFAULT: &str =
include_str!("../../templates/collaboration_mode/default.md");
pub(super) fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
vec![
plan_preset(),
code_preset(),
pair_programming_preset(),
execute_preset(),
]
vec![plan_preset(), default_preset()]
}
#[cfg(any(test, feature = "test-support"))]
@@ -33,32 +25,12 @@ fn plan_preset() -> CollaborationModeMask {
}
}
fn code_preset() -> CollaborationModeMask {
fn default_preset() -> CollaborationModeMask {
CollaborationModeMask {
name: "Code".to_string(),
mode: Some(ModeKind::Code),
name: "Default".to_string(),
mode: Some(ModeKind::Default),
model: None,
reasoning_effort: None,
developer_instructions: Some(Some(COLLABORATION_MODE_CODE.to_string())),
}
}
fn pair_programming_preset() -> CollaborationModeMask {
CollaborationModeMask {
name: "Pair Programming".to_string(),
mode: Some(ModeKind::PairProgramming),
model: None,
reasoning_effort: Some(Some(ReasoningEffort::Medium)),
developer_instructions: Some(Some(COLLABORATION_MODE_PAIR_PROGRAMMING.to_string())),
}
}
fn execute_preset() -> CollaborationModeMask {
CollaborationModeMask {
name: "Execute".to_string(),
mode: Some(ModeKind::Execute),
model: None,
reasoning_effort: Some(Some(ReasoningEffort::High)),
developer_instructions: Some(Some(COLLABORATION_MODE_EXECUTE.to_string())),
developer_instructions: Some(Some(COLLABORATION_MODE_DEFAULT.to_string())),
}
}
@@ -39,9 +39,7 @@ impl ToolHandler for RequestUserInputHandler {
let mode = session.collaboration_mode().await.mode;
if !matches!(mode, ModeKind::Plan | ModeKind::PairProgramming) {
let mode_name = match mode {
ModeKind::Code => "Code",
ModeKind::Execute => "Execute",
ModeKind::Custom => "Custom",
ModeKind::Default | ModeKind::Execute => "Default",
ModeKind::Plan | ModeKind::PairProgramming => unreachable!(),
};
return Err(FunctionCallError::RespondToModel(format!(
@@ -1 +0,0 @@
you are now in code mode.
@@ -0,0 +1 @@
you are now in default mode.
+1 -1
View File
@@ -847,7 +847,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
.await?;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.1".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
@@ -37,7 +37,7 @@ fn collab_mode_with_mode_and_instructions(
}
fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode {
collab_mode_with_mode_and_instructions(ModeKind::Custom, instructions)
collab_mode_with_mode_and_instructions(ModeKind::Default, instructions)
}
fn developer_texts(input: &[Value]) -> Vec<String> {
@@ -427,7 +427,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
let req2 = mount_sse_once(&server, sse_completed("resp-2")).await;
let test = test_codex().build(&server).await?;
let code_text = "code mode instructions";
let default_text = "default mode instructions";
let plan_text = "plan mode instructions";
test.codex
@@ -440,8 +440,8 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
effort: None,
summary: None,
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Code,
Some(code_text),
ModeKind::Default,
Some(default_text),
)),
personality: None,
})
@@ -488,9 +488,9 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
let input = req2.single_request().input();
let dev_texts = developer_texts(&input);
let code_text = collab_xml(code_text);
let default_text = collab_xml(default_text);
let plan_text = collab_xml(plan_text);
assert_eq!(count_exact(&dev_texts, &code_text), 1);
assert_eq!(count_exact(&dev_texts, &default_text), 1);
assert_eq!(count_exact(&dev_texts, &plan_text), 1);
Ok(())
@@ -517,7 +517,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
effort: None,
summary: None,
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Code,
ModeKind::Default,
Some(collab_text),
)),
personality: None,
@@ -545,7 +545,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
effort: None,
summary: None,
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Code,
ModeKind::Default,
Some(collab_text),
)),
personality: None,
@@ -24,7 +24,7 @@ use tempfile::TempDir;
fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMode {
CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.1".to_string(),
reasoning_effort: None,
+1 -1
View File
@@ -412,7 +412,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
let TestCodex { codex, .. } = test_codex().build(&server).await?;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.1".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
+15 -19
View File
@@ -71,6 +71,15 @@ fn call_output_content_and_success(
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> {
request_user_input_round_trip_for_mode(ModeKind::Plan).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_works_in_pair_mode() -> anyhow::Result<()> {
request_user_input_round_trip_for_mode(ModeKind::PairProgramming).await
}
async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -134,7 +143,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
effort: None,
summary: ReasoningSummary::Auto,
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Plan,
mode,
settings: Settings {
model: session_configured.model.clone(),
reasoning_effort: None,
@@ -273,8 +282,8 @@ where
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Execute", |model| CollaborationMode {
async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<()> {
assert_request_user_input_rejected("Default", |model| CollaborationMode {
mode: ModeKind::Execute,
settings: Settings {
model,
@@ -286,22 +295,9 @@ async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_code_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Code", |model| CollaborationMode {
mode: ModeKind::Code,
settings: Settings {
model,
reasoning_effort: None,
developer_instructions: None,
},
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_custom_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Custom", |model| CollaborationMode {
mode: ModeKind::Custom,
async fn request_user_input_rejected_in_default_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Default", |model| CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort: None,
@@ -117,7 +117,7 @@ fn task_started_produces_turn_started_event() {
"t1",
EventMsg::TurnStarted(codex_core::protocol::TurnStartedEvent {
model_context_window: Some(32_000),
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
));
+26 -4
View File
@@ -173,10 +173,23 @@ pub enum AltScreenMode {
pub enum ModeKind {
Plan,
#[default]
Code,
#[serde(
alias = "code",
alias = "pair_programming",
alias = "execute",
alias = "custom"
)]
Default,
#[doc(hidden)]
#[serde(skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
PairProgramming,
#[doc(hidden)]
#[serde(skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
Execute,
Custom,
}
/// Collaboration mode for a Codex session.
@@ -276,7 +289,7 @@ mod tests {
#[test]
fn apply_mask_can_clear_optional_fields() {
let mode = CollaborationMode {
mode: ModeKind::Code,
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.2-codex".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
@@ -292,7 +305,7 @@ mod tests {
};
let expected = CollaborationMode {
mode: ModeKind::Code,
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.2-codex".to_string(),
reasoning_effort: None,
@@ -301,4 +314,13 @@ mod tests {
};
assert_eq!(expected, mode.apply_mask(&mask));
}
#[test]
fn mode_kind_deserializes_alias_values_to_default() {
for alias in ["code", "pair_programming", "execute", "custom"] {
let json = format!("\"{alias}\"");
let mode: ModeKind = serde_json::from_str(&json).expect("deserialize mode");
assert_eq!(ModeKind::Default, mode);
}
}
}
+2
View File
@@ -73,7 +73,9 @@ pub(crate) struct FooterProps {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CollaborationModeIndicator {
Plan,
#[allow(dead_code)] // Hidden by current mode filtering; kept for future UI re-enablement.
PairProgramming,
#[allow(dead_code)] // Hidden by current mode filtering; kept for future UI re-enablement.
Execute,
}
+29 -30
View File
@@ -468,7 +468,7 @@ pub(crate) struct ChatWidget {
/// where the overlay may briefly treat new tail content as already cached.
active_cell_revision: u64,
config: Config,
/// The unmasked collaboration mode settings (always Custom mode).
/// The unmasked collaboration mode settings (always Default mode).
///
/// Masks are applied on top of this base mode to derive the effective mode.
current_collaboration_mode: CollaborationMode,
@@ -1114,8 +1114,8 @@ impl ChatWidget {
}
fn open_plan_implementation_prompt(&mut self) {
let code_mask = collaboration_modes::code_mask(self.models_manager.as_ref());
let (implement_actions, implement_disabled_reason) = match code_mask {
let default_mask = collaboration_modes::default_mode_mask(self.models_manager.as_ref());
let (implement_actions, implement_disabled_reason) = match default_mask {
Some(mask) => {
let user_text = PLAN_IMPLEMENTATION_CODING_MESSAGE.to_string();
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
@@ -1126,13 +1126,13 @@ impl ChatWidget {
})];
(actions, None)
}
None => (Vec::new(), Some("Code mode unavailable".to_string())),
None => (Vec::new(), Some("Default mode unavailable".to_string())),
};
let items = vec![
SelectionItem {
name: PLAN_IMPLEMENTATION_YES.to_string(),
description: Some("Switch to Code and start coding.".to_string()),
description: Some("Switch to Default and start coding.".to_string()),
selected_description: None,
is_current: false,
actions: implement_actions,
@@ -2223,15 +2223,15 @@ impl ChatWidget {
.as_ref()
.and_then(|mask| mask.model.clone())
.unwrap_or_else(|| model_for_header.clone());
let fallback_custom = Settings {
let fallback_default = Settings {
model: header_model.clone(),
reasoning_effort: None,
developer_instructions: None,
};
// Collaboration modes start in Custom mode (not activated).
// Collaboration modes start in Default mode.
let current_collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
mode: ModeKind::Default,
settings: fallback_default,
};
let active_cell = Some(Self::placeholder_session_header_cell(&config));
@@ -2368,15 +2368,15 @@ impl ChatWidget {
.as_ref()
.and_then(|mask| mask.model.clone())
.unwrap_or_else(|| model_for_header.clone());
let fallback_custom = Settings {
let fallback_default = Settings {
model: header_model.clone(),
reasoning_effort: None,
developer_instructions: None,
};
// Collaboration modes start in Custom mode (not activated).
// Collaboration modes start in Default mode.
let current_collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
mode: ModeKind::Default,
settings: fallback_default,
};
let active_cell = Some(Self::placeholder_session_header_cell(&config));
@@ -2504,15 +2504,15 @@ impl ChatWidget {
let codex_op_tx =
spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone());
let fallback_custom = Settings {
let fallback_default = Settings {
model: header_model.clone(),
reasoning_effort: None,
developer_instructions: None,
};
// Collaboration modes start in Custom mode (not activated).
// Collaboration modes start in Default mode.
let current_collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
settings: fallback_custom,
mode: ModeKind::Default,
settings: fallback_default,
};
let mut widget = Self {
@@ -5216,10 +5216,14 @@ impl ChatWidget {
self.bottom_pane.set_collaboration_modes_enabled(enabled);
let settings = self.current_collaboration_mode.settings.clone();
self.current_collaboration_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings,
};
self.active_collaboration_mask = None;
self.active_collaboration_mask = if enabled {
collaboration_modes::default_mask(self.models_manager.as_ref())
} else {
None
};
self.update_collaboration_mode_indicator();
self.refresh_model_display();
self.request_redraw();
@@ -5399,7 +5403,7 @@ impl ChatWidget {
self.active_collaboration_mask
.as_ref()
.and_then(|mask| mask.mode)
.unwrap_or(ModeKind::Custom)
.unwrap_or(ModeKind::Default)
}
fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
@@ -5446,10 +5450,8 @@ impl ChatWidget {
}
match self.active_mode_kind() {
ModeKind::Plan => Some("Plan"),
ModeKind::Code => Some("Code"),
ModeKind::PairProgramming => Some("Pair Programming"),
ModeKind::Execute => Some("Execute"),
ModeKind::Custom => None,
ModeKind::Default => Some("Default"),
ModeKind::PairProgramming | ModeKind::Execute => None,
}
}
@@ -5459,10 +5461,7 @@ impl ChatWidget {
}
match self.active_mode_kind() {
ModeKind::Plan => Some(CollaborationModeIndicator::Plan),
ModeKind::Code => None,
ModeKind::PairProgramming => Some(CollaborationModeIndicator::PairProgramming),
ModeKind::Execute => Some(CollaborationModeIndicator::Execute),
ModeKind::Custom => None,
ModeKind::Default | ModeKind::PairProgramming | ModeKind::Execute => None,
}
}
@@ -5485,7 +5484,7 @@ impl ChatWidget {
}
}
/// Cycle to the next collaboration mode variant (Plan -> Code -> Plan).
/// Cycle to the next collaboration mode variant (Plan -> Default -> Plan).
fn cycle_collaboration_mode(&mut self) {
if !self.collaboration_modes_enabled() {
return;
@@ -5501,7 +5500,7 @@ impl ChatWidget {
/// Update the active collaboration mask.
///
/// When collaboration modes are enabled and a preset is selected (not Custom),
/// When collaboration modes are enabled and a preset is selected,
/// the current mode is attached to submissions as `Op::UserTurn { collaboration_mode: Some(...) }`.
pub(crate) fn set_collaboration_mask(&mut self, mask: CollaborationModeMask) {
if !self.collaboration_modes_enabled() {
@@ -4,7 +4,7 @@ expression: popup
---
Implement this plan?
1. Yes, implement this plan Switch to Code and start coding.
1. Yes, implement this plan Switch to Default and start coding.
2. No, stay in Plan mode Continue planning with the model.
Press enter to confirm or esc to go back
@@ -4,7 +4,7 @@ expression: popup
---
Implement this plan?
1. Yes, implement this plan Switch to Code and start coding.
1. Yes, implement this plan Switch to Default and start coding.
2. No, stay in Plan mode Continue planning with the model.
Press enter to confirm or esc to go back
+69 -34
View File
@@ -824,7 +824,7 @@ async fn make_chatwidget_manual(
let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone()));
let reasoning_effort = None;
let base_mode = CollaborationMode {
mode: ModeKind::Custom,
mode: ModeKind::Default,
settings: Settings {
model: resolved_model.clone(),
reasoning_effort,
@@ -1255,7 +1255,7 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() {
panic!("expected SubmitUserMessageWithMode, got {event:?}");
};
assert_eq!(text, PLAN_IMPLEMENTATION_CODING_MESSAGE);
assert_eq!(collaboration_mode.mode, Some(ModeKind::Code));
assert_eq!(collaboration_mode.mode, Some(ModeKind::Default));
}
#[tokio::test]
@@ -1264,22 +1264,22 @@ async fn submit_user_message_with_mode_sets_coding_collaboration_mode() {
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, true);
let code_mode = collaboration_modes::code_mask(chat.models_manager.as_ref())
.expect("expected code collaboration mode");
chat.submit_user_message_with_mode("Implement the plan.".to_string(), code_mode);
let default_mode = collaboration_modes::default_mode_mask(chat.models_manager.as_ref())
.expect("expected default collaboration mode");
chat.submit_user_message_with_mode("Implement the plan.".to_string(), default_mode);
match next_submit_op(&mut op_rx) {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Code,
mode: ModeKind::Default,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with code collab mode, got {other:?}")
panic!("expected Op::UserTurn with default collab mode, got {other:?}")
}
}
}
@@ -2100,7 +2100,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -2135,7 +2135,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -2346,7 +2346,7 @@ async fn collab_mode_shift_tab_cycles_only_when_enabled_and_idle() {
let initial = chat.current_collaboration_mode().clone();
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert_eq!(chat.current_collaboration_mode(), &initial);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Custom);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default);
chat.set_feature_enabled(Feature::CollaborationModes, true);
@@ -2355,7 +2355,7 @@ async fn collab_mode_shift_tab_cycles_only_when_enabled_and_idle() {
assert_eq!(chat.current_collaboration_mode(), &initial);
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Code);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default);
assert_eq!(chat.current_collaboration_mode(), &initial);
chat.on_task_started();
@@ -2391,7 +2391,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Code,
mode: ModeKind::Default,
..
}),
personality: None,
@@ -2409,7 +2409,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Code,
mode: ModeKind::Default,
..
}),
personality: None,
@@ -2513,7 +2513,7 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
};
let chat = ChatWidget::new(init, thread_manager);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Code);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default);
assert_eq!(chat.current_model(), resolved_model);
}
@@ -2593,7 +2593,7 @@ async fn set_reasoning_effort_updates_active_collaboration_mask() {
}
#[tokio::test]
async fn collab_mode_is_not_sent_until_selected() {
async fn collab_mode_is_sent_after_enabling() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, true);
@@ -2603,12 +2603,14 @@ async fn collab_mode_is_not_sent_until_selected() {
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) {
Op::UserTurn {
collaboration_mode,
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Default,
..
}),
personality: None,
..
} => {
assert_eq!(collaboration_mode, None);
}
} => {}
other => {
panic!("expected Op::UserTurn, got {other:?}")
}
@@ -2616,11 +2618,44 @@ async fn collab_mode_is_not_sent_until_selected() {
}
#[tokio::test]
async fn collab_mode_enabling_keeps_custom_until_selected() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
async fn collab_mode_toggle_on_applies_default_preset() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
chat.thread_id = Some(ThreadId::new());
chat.bottom_pane
.set_composer_text("before toggle".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) {
Op::UserTurn {
collaboration_mode: None,
personality: None,
..
} => {}
other => panic!("expected Op::UserTurn without collaboration_mode, got {other:?}"),
}
chat.set_feature_enabled(Feature::CollaborationModes, true);
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Custom);
assert_eq!(chat.current_collaboration_mode().mode, ModeKind::Custom);
chat.bottom_pane
.set_composer_text("after toggle".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match next_submit_op(&mut op_rx) {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Default,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with default collaboration_mode, got {other:?}")
}
}
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default);
assert_eq!(chat.current_collaboration_mode().mode, ModeKind::Default);
}
#[tokio::test]
@@ -2968,7 +3003,7 @@ async fn interrupted_turn_error_message_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -3985,7 +4020,7 @@ async fn interrupt_clears_unified_exec_wait_streak_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -4059,7 +4094,7 @@ async fn ui_snapshots_small_heights_task_running() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
chat.handle_codex_event(Event {
@@ -4091,7 +4126,7 @@ async fn status_widget_and_approval_modal_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
// Provide a deterministic header for the status line.
@@ -4144,7 +4179,7 @@ async fn status_widget_active_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
// Provide a deterministic header via a bold reasoning chunk.
@@ -4194,7 +4229,7 @@ async fn mcp_startup_complete_does_not_clear_running_task() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -4751,7 +4786,7 @@ async fn stream_recovery_restores_previous_status_header() {
id: "task".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
drain_insert_history(&mut rx);
@@ -4789,7 +4824,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
id: "s1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
@@ -4984,7 +5019,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
chat.handle_codex_event(Event {
@@ -5032,7 +5067,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
// Build a vt100 visual from the history insertions only (no UI overlay)
@@ -5122,7 +5157,7 @@ async fn chatwidget_tall() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
model_context_window: None,
collaboration_mode_kind: ModeKind::Custom,
collaboration_mode_kind: ModeKind::Default,
}),
});
for i in 0..30 {
+4 -4
View File
@@ -3,7 +3,7 @@ use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ModeKind;
fn is_tui_mode(kind: ModeKind) -> bool {
matches!(kind, ModeKind::Plan | ModeKind::Code)
matches!(kind, ModeKind::Plan | ModeKind::Default)
}
fn filtered_presets(models_manager: &ModelsManager) -> Vec<CollaborationModeMask> {
@@ -22,7 +22,7 @@ pub(crate) fn default_mask(models_manager: &ModelsManager) -> Option<Collaborati
let presets = filtered_presets(models_manager);
presets
.iter()
.find(|mask| mask.mode == Some(ModeKind::Code))
.find(|mask| mask.mode == Some(ModeKind::Default))
.cloned()
.or_else(|| presets.into_iter().next())
}
@@ -56,8 +56,8 @@ pub(crate) fn next_mask(
presets.get(next_index).cloned()
}
pub(crate) fn code_mask(models_manager: &ModelsManager) -> Option<CollaborationModeMask> {
mask_for_kind(models_manager, ModeKind::Code)
pub(crate) fn default_mode_mask(models_manager: &ModelsManager) -> Option<CollaborationModeMask> {
mask_for_kind(models_manager, ModeKind::Default)
}
pub(crate) fn plan_mask(models_manager: &ModelsManager) -> Option<CollaborationModeMask> {