diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 65648bbdb..28954c7cd 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2420,6 +2420,8 @@ pub struct ToolRequestUserInputQuestion { pub id: String, pub header: String, pub question: String, + #[serde(default)] + pub is_other: bool, pub options: Option>, } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 0cb6fb1bc..20e7db762 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -278,6 +278,7 @@ pub(crate) async fn apply_bespoke_event_handling( id: question.id, header: question.header, question: question.question, + is_other: question.is_other, options: question.options.map(|options| { options .into_iter() diff --git a/codex-rs/app-server/tests/common/responses.rs b/codex-rs/app-server/tests/common/responses.rs index e15319e02..2a5b5d215 100644 --- a/codex-rs/app-server/tests/common/responses.rs +++ b/codex-rs/app-server/tests/common/responses.rs @@ -67,6 +67,7 @@ pub fn create_request_user_input_sse_response(call_id: &str) -> anyhow::Result ToolSpec { let options_schema = JsonSchema::Array { description: Some( - "Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Only include \"Other\" option if we want to include a free form option. If the question is free form in nature, please do not have any option." + "Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; use isOther on the question to request a free form choice. If the question is free form in nature, please do not have any option." .to_string(), ), items: Box::new(JsonSchema::Object { @@ -586,6 +586,15 @@ fn create_request_user_input_tool() -> ToolSpec { description: Some("Single-sentence prompt shown to the user.".to_string()), }, ); + question_props.insert( + "isOther".to_string(), + JsonSchema::Boolean { + description: Some( + "True when this question should include a free-form \"Other\" option. Otherwise false." + .to_string(), + ), + }, + ); question_props.insert("options".to_string(), options_schema); let questions_schema = JsonSchema::Array { @@ -596,6 +605,7 @@ fn create_request_user_input_tool() -> ToolSpec { "id".to_string(), "header".to_string(), "question".to_string(), + "isOther".to_string(), ]), additional_properties: Some(false.into()), }), diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 74d3fc98f..2d81fc9ad 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -94,6 +94,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> "id": "confirm_path", "header": "Confirm", "question": "Proceed with the plan?", + "isOther": false, "options": [{ "label": "Yes (Recommended)", "description": "Continue the current plan." @@ -213,6 +214,7 @@ where "id": "confirm_path", "header": "Confirm", "question": "Proceed with the plan?", + "isOther": false, "options": [{ "label": "Yes (Recommended)", "description": "Continue the current plan." diff --git a/codex-rs/docs/protocol_v1.md b/codex-rs/docs/protocol_v1.md index f5579008b..ee49cac0a 100644 --- a/codex-rs/docs/protocol_v1.md +++ b/codex-rs/docs/protocol_v1.md @@ -75,7 +75,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc - `EventMsg` - `EventMsg::AgentMessage` – Messages from the `Model` - `EventMsg::ExecApprovalRequest` – Request approval from user to execute a command - - `EventMsg::RequestUserInput` – Request user input for a tool call + - `EventMsg::RequestUserInput` – Request user input for a tool call (questions can include options plus `isOther` to add a free-form choice) - `EventMsg::TurnComplete` – A turn completed successfully - `EventMsg::Error` – A turn stopped with an error - `EventMsg::Warning` – A non-fatal warning that the client should surface to the user diff --git a/codex-rs/protocol/src/request_user_input.rs b/codex-rs/protocol/src/request_user_input.rs index 7c0e02c59..0e58348da 100644 --- a/codex-rs/protocol/src/request_user_input.rs +++ b/codex-rs/protocol/src/request_user_input.rs @@ -16,6 +16,10 @@ pub struct RequestUserInputQuestion { pub id: String, pub header: String, pub question: String, + #[serde(rename = "isOther", default)] + #[schemars(rename = "isOther")] + #[ts(rename = "isOther")] + pub is_other: bool, #[serde(skip_serializing_if = "Option::is_none")] pub options: Option>, } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index a01aa2296..aaa639177 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -508,6 +508,7 @@ mod tests { id: id.to_string(), header: header.to_string(), question: "Choose an option.".to_string(), + is_other: false, options: Some(vec![ RequestUserInputQuestionOption { label: "Option 1".to_string(), @@ -530,6 +531,7 @@ mod tests { id: id.to_string(), header: header.to_string(), question: "Share details.".to_string(), + is_other: false, options: None, } } @@ -696,6 +698,7 @@ mod tests { id: "q1".to_string(), header: "Next Step".to_string(), question: "What would you like to do next?".to_string(), + is_other: false, options: Some(vec![ RequestUserInputQuestionOption { label: "Discuss a code change (Recommended)".to_string(),