Have a coding mode and only show coding and plan (#9802)

This commit is contained in:
Ahmed Ibrahim
2026-01-23 19:28:49 -08:00
committed by GitHub
parent 77222492f9
commit b3127e2eeb
12 changed files with 99 additions and 44 deletions

View File

@@ -1,7 +1,7 @@
//! 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, pair programming, and execute modes with their default model and reasoning
//! 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.
#![allow(clippy::unwrap_used)]
@@ -45,7 +45,12 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> {
let CollaborationModeListResponse { data: items } =
to_response::<CollaborationModeListResponse>(response)?;
let expected = vec![plan_preset(), pair_programming_preset(), execute_preset()];
let expected = vec![
plan_preset(),
code_preset(),
pair_programming_preset(),
execute_preset(),
];
assert_eq!(expected, items);
Ok(())
}
@@ -74,6 +79,15 @@ fn pair_programming_preset() -> CollaborationMode {
.unwrap()
}
/// Builds the code preset that the list response is expected to return.
fn code_preset() -> CollaborationMode {
let presets = test_builtin_collaboration_mode_presets();
presets
.into_iter()
.find(|p| p.mode == 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

View File

@@ -350,6 +350,7 @@
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
"plan",
"code",
"pair_programming",
"execute",
"custom"

View File

@@ -105,7 +105,7 @@ pub enum Feature {
Connectors,
/// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
Steer,
/// Enable collaboration modes (Plan, Pair Programming, Execute).
/// Enable collaboration modes (Plan, Code, Pair Programming, Execute).
CollaborationModes,
/// Use the Responses API WebSocket transport for OpenAI by default.
ResponsesWebsockets,

View File

@@ -4,13 +4,19 @@ use codex_protocol::config_types::Settings;
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");
pub(super) fn builtin_collaboration_mode_presets() -> Vec<CollaborationMode> {
vec![plan_preset(), pair_programming_preset(), execute_preset()]
vec![
plan_preset(),
code_preset(),
pair_programming_preset(),
execute_preset(),
]
}
#[cfg(any(test, feature = "test-support"))]
@@ -29,6 +35,17 @@ fn plan_preset() -> CollaborationMode {
}
}
fn code_preset() -> CollaborationMode {
CollaborationMode {
mode: ModeKind::Code,
settings: Settings {
model: "gpt-5.2-codex".to_string(),
reasoning_effort: Some(ReasoningEffort::Medium),
developer_instructions: Some(COLLABORATION_MODE_CODE.to_string()),
},
}
}
fn pair_programming_preset() -> CollaborationMode {
CollaborationMode {
mode: ModeKind::PairProgramming,

View File

@@ -0,0 +1 @@
you are now in code mode.

View File

@@ -156,6 +156,7 @@ pub enum AltScreenMode {
#[serde(rename_all = "snake_case")]
pub enum ModeKind {
Plan,
Code,
PairProgramming,
Execute,
Custom,

View File

@@ -49,6 +49,7 @@ pub(crate) struct FooterProps {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CollaborationModeIndicator {
Plan,
Code,
PairProgramming,
Execute,
}
@@ -64,6 +65,7 @@ impl CollaborationModeIndicator {
};
match self {
CollaborationModeIndicator::Plan => format!("Plan mode{suffix}"),
CollaborationModeIndicator::Code => format!("Code mode{suffix}"),
CollaborationModeIndicator::PairProgramming => {
format!("Pair Programming mode{suffix}")
}
@@ -75,6 +77,7 @@ impl CollaborationModeIndicator {
let label = self.label(show_cycle_hint);
match self {
CollaborationModeIndicator::Plan => Span::from(label).magenta(),
CollaborationModeIndicator::Code => Span::from(label).cyan(),
CollaborationModeIndicator::PairProgramming => Span::from(label).cyan(),
CollaborationModeIndicator::Execute => Span::from(label).dim(),
}

View File

@@ -122,7 +122,7 @@ const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?";
const PLAN_IMPLEMENTATION_YES: &str = "Yes, implement this plan";
const PLAN_IMPLEMENTATION_NO: &str = "No, stay in Plan mode";
const PLAN_IMPLEMENTATION_EXECUTE_MESSAGE: &str = "Implement the plan.";
const PLAN_IMPLEMENTATION_CODING_MESSAGE: &str = "Implement the plan.";
use crate::app_event::AppEvent;
use crate::app_event::ExitMode;
@@ -932,10 +932,10 @@ impl ChatWidget {
}
fn open_plan_implementation_prompt(&mut self) {
let execute_mode = collaboration_modes::execute_mode(self.models_manager.as_ref());
let (implement_actions, implement_disabled_reason) = match execute_mode {
let code_mode = collaboration_modes::code_mode(self.models_manager.as_ref());
let (implement_actions, implement_disabled_reason) = match code_mode {
Some(collaboration_mode) => {
let user_text = PLAN_IMPLEMENTATION_EXECUTE_MESSAGE.to_string();
let user_text = PLAN_IMPLEMENTATION_CODING_MESSAGE.to_string();
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
tx.send(AppEvent::SubmitUserMessageWithMode {
text: user_text.clone(),
@@ -944,13 +944,13 @@ impl ChatWidget {
})];
(actions, None)
}
None => (Vec::new(), Some("Execute mode unavailable".to_string())),
None => (Vec::new(), Some("Code mode unavailable".to_string())),
};
let items = vec![
SelectionItem {
name: PLAN_IMPLEMENTATION_YES.to_string(),
description: Some("Switch to Execute and start coding.".to_string()),
description: Some("Switch to Code and start coding.".to_string()),
selected_description: None,
is_current: false,
actions: implement_actions,
@@ -3549,7 +3549,7 @@ impl ChatWidget {
}
pub(crate) fn open_collaboration_modes_popup(&mut self) {
let presets = self.models_manager.list_collaboration_modes();
let presets = collaboration_modes::presets_for_tui(self.models_manager.as_ref());
if presets.is_empty() {
self.add_info_message(
"No collaboration modes are available right now.".to_string(),
@@ -3563,6 +3563,7 @@ impl ChatWidget {
.map(|preset| {
let name = match preset.mode {
ModeKind::Plan => "Plan",
ModeKind::Code => "Code",
ModeKind::PairProgramming => "Pair Programming",
ModeKind::Execute => "Execute",
ModeKind::Custom => "Custom",
@@ -4641,6 +4642,7 @@ impl ChatWidget {
}
match self.stored_collaboration_mode.mode {
ModeKind::Plan => Some("Plan"),
ModeKind::Code => Some("Code"),
ModeKind::PairProgramming => Some("Pair Programming"),
ModeKind::Execute => Some("Execute"),
ModeKind::Custom => None,
@@ -4653,6 +4655,7 @@ impl ChatWidget {
}
match self.stored_collaboration_mode.mode {
ModeKind::Plan => Some(CollaborationModeIndicator::Plan),
ModeKind::Code => Some(CollaborationModeIndicator::Code),
ModeKind::PairProgramming => Some(CollaborationModeIndicator::PairProgramming),
ModeKind::Execute => Some(CollaborationModeIndicator::Execute),
ModeKind::Custom => None,
@@ -4664,7 +4667,7 @@ impl ChatWidget {
self.bottom_pane.set_collaboration_mode_indicator(indicator);
}
/// Cycle to the next collaboration mode variant (Plan -> PairProgramming -> Execute -> Plan).
/// Cycle to the next collaboration mode variant (Plan -> Code -> Plan).
fn cycle_collaboration_mode(&mut self) {
if !self.collaboration_modes_enabled() {
return;

View File

@@ -4,7 +4,7 @@ expression: popup
---
Implement this plan?
1. Yes, implement this plan Switch to Execute and start coding.
1. Yes, implement this plan Switch to Code and start coding.
2. No, stay in Plan mode Continue planning with the model.
Press enter to confirm or esc to go back

View File

@@ -4,7 +4,7 @@ expression: popup
---
Implement this plan?
1. Yes, implement this plan Switch to Execute and start coding.
1. Yes, implement this plan Switch to Code and start coding.
2. No, stay in Plan mode Continue planning with the model.
Press enter to confirm or esc to go back

View File

@@ -1213,32 +1213,32 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() {
else {
panic!("expected SubmitUserMessageWithMode, got {event:?}");
};
assert_eq!(text, PLAN_IMPLEMENTATION_EXECUTE_MESSAGE);
assert_eq!(collaboration_mode.mode, ModeKind::Execute);
assert_eq!(text, PLAN_IMPLEMENTATION_CODING_MESSAGE);
assert_eq!(collaboration_mode.mode, ModeKind::Code);
}
#[tokio::test]
async fn submit_user_message_with_mode_sets_execute_collaboration_mode() {
async fn submit_user_message_with_mode_sets_coding_collaboration_mode() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.thread_id = Some(ThreadId::new());
chat.set_feature_enabled(Feature::CollaborationModes, true);
let execute_mode = collaboration_modes::execute_mode(chat.models_manager.as_ref())
.expect("expected execute collaboration mode");
chat.submit_user_message_with_mode("Implement the plan.".to_string(), execute_mode);
let code_mode = collaboration_modes::code_mode(chat.models_manager.as_ref())
.expect("expected code collaboration mode");
chat.submit_user_message_with_mode("Implement the plan.".to_string(), code_mode);
match next_submit_op(&mut op_rx) {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::Execute,
mode: ModeKind::Code,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with execute collab mode, got {other:?}")
panic!("expected Op::UserTurn with code collab mode, got {other:?}")
}
}
}
@@ -2229,10 +2229,10 @@ async fn collab_mode_shift_tab_cycles_only_when_enabled_and_idle() {
chat.set_feature_enabled(Feature::CollaborationModes, true);
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Execute);
assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan);
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Plan);
assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Code);
chat.on_task_started();
let before = chat.stored_collaboration_mode.clone();
@@ -2267,14 +2267,14 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
mode: ModeKind::Code,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}")
panic!("expected Op::UserTurn with code collab mode, got {other:?}")
}
}
@@ -2285,20 +2285,20 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
mode: ModeKind::Code,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}")
panic!("expected Op::UserTurn with code collab mode, got {other:?}")
}
}
}
#[tokio::test]
async fn collab_mode_defaults_to_pair_programming_when_enabled() {
async fn collab_mode_defaults_to_coding_when_enabled() {
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);
@@ -2310,26 +2310,23 @@ async fn collab_mode_defaults_to_pair_programming_when_enabled() {
Op::UserTurn {
collaboration_mode:
Some(CollaborationMode {
mode: ModeKind::PairProgramming,
mode: ModeKind::Code,
..
}),
personality: None,
..
} => {}
other => {
panic!("expected Op::UserTurn with pair programming collab mode, got {other:?}")
panic!("expected Op::UserTurn with code collab mode, got {other:?}")
}
}
}
#[tokio::test]
async fn collab_mode_enabling_sets_pair_programming_default() {
async fn collab_mode_enabling_sets_coding_default() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.set_feature_enabled(Feature::CollaborationModes, true);
assert_eq!(
chat.stored_collaboration_mode.mode,
ModeKind::PairProgramming
);
assert_eq!(chat.stored_collaboration_mode.mode, ModeKind::Code);
}
#[tokio::test]

View File

@@ -6,11 +6,27 @@ fn mode_kind(mode: &CollaborationMode) -> ModeKind {
mode.mode
}
fn is_tui_mode(kind: ModeKind) -> bool {
matches!(kind, ModeKind::Plan | ModeKind::Code)
}
fn filtered_presets(models_manager: &ModelsManager) -> Vec<CollaborationMode> {
models_manager
.list_collaboration_modes()
.into_iter()
.filter(|preset| is_tui_mode(mode_kind(preset)))
.collect()
}
pub(crate) fn presets_for_tui(models_manager: &ModelsManager) -> Vec<CollaborationMode> {
filtered_presets(models_manager)
}
pub(crate) fn default_mode(models_manager: &ModelsManager) -> Option<CollaborationMode> {
let presets = models_manager.list_collaboration_modes();
let presets = filtered_presets(models_manager);
presets
.iter()
.find(|preset| preset.mode == ModeKind::PairProgramming)
.find(|preset| preset.mode == ModeKind::Code)
.cloned()
.or_else(|| presets.into_iter().next())
}
@@ -19,7 +35,10 @@ pub(crate) fn mode_for_kind(
models_manager: &ModelsManager,
kind: ModeKind,
) -> Option<CollaborationMode> {
let presets = models_manager.list_collaboration_modes();
if !is_tui_mode(kind) {
return None;
}
let presets = filtered_presets(models_manager);
presets.into_iter().find(|preset| mode_kind(preset) == kind)
}
@@ -32,7 +51,7 @@ pub(crate) fn next_mode(
models_manager: &ModelsManager,
current: &CollaborationMode,
) -> Option<CollaborationMode> {
let presets = models_manager.list_collaboration_modes();
let presets = filtered_presets(models_manager);
if presets.is_empty() {
return None;
}
@@ -44,9 +63,8 @@ pub(crate) fn next_mode(
presets.get(next_index).cloned()
}
pub(crate) fn execute_mode(models_manager: &ModelsManager) -> Option<CollaborationMode> {
models_manager
.list_collaboration_modes()
pub(crate) fn code_mode(models_manager: &ModelsManager) -> Option<CollaborationMode> {
filtered_presets(models_manager)
.into_iter()
.find(|preset| mode_kind(preset) == ModeKind::Execute)
.find(|preset| mode_kind(preset) == ModeKind::Code)
}