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
parent aea38f0f88
commit d509df676b
34 changed files with 205 additions and 250 deletions

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 {