diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 0635b530e..b1e48fe23 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2327,8 +2327,7 @@ pub struct ToolRequestUserInputParams { #[ts(export_to = "v2/")] /// EXPERIMENTAL. Captures a user's answer to a request_user_input question. pub struct ToolRequestUserInputAnswer { - pub selected: Vec, - pub other: Option, + pub answers: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 225b2afac..f9332e6f6 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -1445,8 +1445,7 @@ async fn on_request_user_input_response( ( id, CoreRequestUserInputAnswer { - selected: answer.selected, - other: answer.other, + answers: answer.answers, }, ) }) diff --git a/codex-rs/app-server/tests/suite/v2/request_user_input.rs b/codex-rs/app-server/tests/suite/v2/request_user_input.rs index 8b1f1aedd..c66dd05be 100644 --- a/codex-rs/app-server/tests/suite/v2/request_user_input.rs +++ b/codex-rs/app-server/tests/suite/v2/request_user_input.rs @@ -87,7 +87,7 @@ async fn request_user_input_round_trip() -> Result<()> { request_id, serde_json::json!({ "answers": { - "confirm_path": { "selected": ["yes"], "other": serde_json::Value::Null } + "confirm_path": { "answers": ["yes"] } } }), ) diff --git a/codex-rs/core/templates/collaboration_mode/plan.md b/codex-rs/core/templates/collaboration_mode/plan.md index 9985ba113..d156cf84f 100644 --- a/codex-rs/core/templates/collaboration_mode/plan.md +++ b/codex-rs/core/templates/collaboration_mode/plan.md @@ -33,7 +33,7 @@ Example: "I checked the readme and searched for the feature you mentioned, but d Use `request_user_input` only when you are genuinely blocked on a decision that materially changes the plan (requirements, trade-offs, rollout or risk posture).The maximum number of `request_user_input` tool calls should be **5**. -Only include an "Other" option when a free-form answer is truly useful. If the question is purely free-form, leave `options` unset entirely. +**The options should be mutually exclusive.** Only include an "Other" option when a free-form answer is truly useful. If the question is purely free-form, leave `options` unset entirely. Do **not** use `request_user_input` to ask "is my plan ready?" or "should I proceed?". @@ -112,22 +112,12 @@ A well written and informative plan should be as detailed as a design doc or PRD - tools and frameworks you use, any dependencies you need to install - functions, files, or directories you're likely going to edit -- QUestions that were asked and the responses from users +- Questions that were asked and the responses from users - architecture if the code changes are significant - if developing features, describe the features you are going to build in detail like a PM in a PRD - if you are developing a frontend, describe the design in detail - include a list of todos in markdown format if needed. Please do not include a **plan** step given that we are planning here already -### Output schema - — MUST MATCH _exactly_ +### Plan output -When you present the plan, format the final response as a JSON object with a single key, `plan`, whose value is the full plan text. - -Example: - -```json -{ - "plan": "Title: Schema migration rollout\n\n1. Validate the current schema on staging...\n2. Add the new columns with nullable defaults...\n3. Backfill in batches with feature-flagged writes...\n4. Flip reads to the new fields and monitor...\n5. Remove legacy columns after one full release cycle..." -} -``` - -PLEASE DO NOT confirm the plan with the user before ending. The user will be responsible for telling us to update, iterate or execute the plan. +**The final output should contain the plan and plan only with a good title.** PLEASE DO NOT confirm the plan with the user before ending. The user will be responsible for telling us to update, iterate or execute the plan. The diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index adf9e3e9d..027d7dab7 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -152,8 +152,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> answers.insert( "confirm_path".to_string(), RequestUserInputAnswer { - selected: vec!["yes".to_string()], - other: None, + answers: vec!["yes".to_string()], }, ); let response = RequestUserInputResponse { answers }; @@ -173,7 +172,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> output_json, json!({ "answers": { - "confirm_path": { "selected": ["yes"], "other": Value::Null } + "confirm_path": { "answers": ["yes"] } } }) ); diff --git a/codex-rs/protocol/src/request_user_input.rs b/codex-rs/protocol/src/request_user_input.rs index 44ad3b630..7c0e02c59 100644 --- a/codex-rs/protocol/src/request_user_input.rs +++ b/codex-rs/protocol/src/request_user_input.rs @@ -27,8 +27,7 @@ pub struct RequestUserInputArgs { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestUserInputAnswer { - pub selected: Vec, - pub other: Option, + pub answers: Vec, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] 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 60d91dde5..a01aa2296 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 @@ -2,10 +2,10 @@ //! //! Core behaviors: //! - Each question can be answered by selecting one option and/or providing notes. -//! - When options exist, notes are stored per selected option (notes become "other"). +//! - When options exist, notes are stored per selected option and appended as extra answers. //! - Typing while focused on options jumps into notes to keep freeform input fast. //! - Enter advances to the next question; the last question submits all answers. -//! - Freeform-only questions submit "skipped" when empty. +//! - Freeform-only questions submit an empty answer list when empty. use std::cell::RefCell; use std::collections::HashMap; use std::collections::VecDeque; @@ -278,7 +278,7 @@ impl RequestUserInputOverlay { } else { answer_state.selected }; - // Notes map to "other". When options exist, notes are per selected option. + // Notes are appended as extra answers. When options exist, notes are per selected option. let notes = if options.is_some_and(|opts| !opts.is_empty()) { selected_idx .and_then(|selected| answer_state.option_notes.get(selected)) @@ -294,18 +294,15 @@ impl RequestUserInputOverlay { .and_then(|opts| opts.get(selected_idx)) .map(|opt| opt.label.clone()) }); - let selected = selected_label.into_iter().collect::>(); - // For option questions, only send notes when present. - let other = if notes.is_empty() && options.is_some_and(|opts| !opts.is_empty()) { - None - } else if notes.is_empty() && selected.is_empty() { - Some("skipped".to_string()) - } else { - Some(notes) - }; + let mut answer_list = selected_label.into_iter().collect::>(); + if !notes.is_empty() { + answer_list.push(format!("user_note: {notes}")); + } answers.insert( question.id.clone(), - RequestUserInputAnswer { selected, other }, + RequestUserInputAnswer { + answers: answer_list, + }, ); } self.app_event_tx @@ -605,12 +602,11 @@ mod tests { }; assert_eq!(id, "turn-1"); let answer = response.answers.get("q1").expect("answer missing"); - assert_eq!(answer.selected, vec!["Option 1".to_string()]); - assert_eq!(answer.other, None); + assert_eq!(answer.answers, vec!["Option 1".to_string()]); } #[test] - fn freeform_questions_submit_skipped_when_empty() { + fn freeform_questions_submit_empty_when_empty() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( request_event("turn-1", vec![question_without_options("q1", "Notes")]), @@ -624,8 +620,7 @@ mod tests { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); - assert_eq!(answer.selected, Vec::::new()); - assert_eq!(answer.other, Some("skipped".to_string())); + assert_eq!(answer.answers, Vec::::new()); } #[test] @@ -654,8 +649,13 @@ mod tests { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); - assert_eq!(answer.selected, vec!["Option 2".to_string()]); - assert_eq!(answer.other, Some("Notes for option 2".to_string())); + assert_eq!( + answer.answers, + vec![ + "Option 2".to_string(), + "user_note: Notes for option 2".to_string(), + ] + ); } #[test]