diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 2be0bb859c..51f17b1162 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -43,6 +43,7 @@ use tracing::warn; const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ "apps", "plugins", + "selection_quote_replies", "tool_search", "tool_suggest", "tool_call_mcp_elicitation", diff --git a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs index 3063830d5a..90d2c6d8b1 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs @@ -165,6 +165,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re &mut mcp, BTreeMap::from([ ("plugins".to_string(), true), + ("selection_quote_replies".to_string(), true), ("tool_search".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), @@ -177,6 +178,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re ExperimentalFeatureEnablementSetResponse { enablement: BTreeMap::from([ ("plugins".to_string(), true), + ("selection_quote_replies".to_string(), true), ("tool_search".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), @@ -200,6 +202,13 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re .and_then(|features| features.get("plugins")), Some(&json!(true)) ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("selection_quote_replies")), + Some(&json!(true)) + ); assert_eq!( config .additional @@ -283,7 +292,9 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() - assert!( error .message - .contains("apps, plugins, tool_search, tool_suggest, tool_call_mcp_elicitation"), + .contains( + "apps, plugins, selection_quote_replies, tool_search, tool_suggest, tool_call_mcp_elicitation" + ), "{}", error.message ); diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 853f0235e0..adaf1f92d6 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -461,6 +461,9 @@ "search_tool": { "type": "boolean" }, + "selection_quote_replies": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, @@ -2074,6 +2077,9 @@ "search_tool": { "type": "boolean" }, + "selection_quote_replies": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 0aa019cb06..ef82362c90 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -176,6 +176,8 @@ pub enum Feature { VoiceTranscription, /// Enable experimental realtime voice conversation mode in the TUI. RealtimeConversation, + /// Enable selection-based quote replies in app-server clients. + SelectionQuoteReplies, /// Route interactive startup to the app-server-backed TUI implementation. TuiAppServer, /// Prevent idle system sleep while a turn is actively running. @@ -819,6 +821,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::SelectionQuoteReplies, + key: "selection_quote_replies", + stage: Stage::Experimental { + name: "Selection quote replies", + menu_description: "Show a quote-reply action when you select text in a conversation and insert the selection into the composer as a quoted reply.", + announcement: "", + }, + default_enabled: false, + }, FeatureSpec { id: Feature::TuiAppServer, key: "tui_app_server", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 4943a1ccf8..44c69cf892 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -150,6 +150,26 @@ fn tool_call_mcp_elicitation_is_stable_and_disabled_by_default() { assert_eq!(Feature::ToolCallMcpElicitation.default_enabled(), false); } +#[test] +fn selection_quote_replies_is_experimental_and_user_toggleable() { + let spec = Feature::SelectionQuoteReplies.info(); + let stage = spec.stage; + + assert!(matches!(stage, Stage::Experimental { .. })); + assert_eq!( + stage.experimental_menu_name(), + Some("Selection quote replies") + ); + assert_eq!( + stage.experimental_menu_description(), + Some( + "Show a quote-reply action when you select text in a conversation and insert the selection into the composer as a quoted reply." + ) + ); + assert_eq!(stage.experimental_announcement(), None); + assert_eq!(Feature::SelectionQuoteReplies.default_enabled(), false); +} + #[test] fn image_detail_original_feature_is_under_development() { assert_eq!(