fix: custom prompt expansion with large pastes (#7154)

### **Summary of Changes**

**What?**
Fix for slash commands (e.g., /prompts:code-review) not being recognized
when large content (>3000 chars) is pasted.
[Bug Report](https://github.com/openai/codex/issues/7047)
**Why?**
With large pastes, slash commands were ignored, so custom prompts
weren't expanded and were submitted as literal text.

**How?**
Refactored the early return block in handle_key_event_without_popup
(lines 957-968).
Instead of returning early after replacing placeholders, the code now
replaces placeholders in the textarea and continues to the normal
submission flow.
This reuses the existing slash command detection and custom prompt
expansion logic (lines 981-1047), avoiding duplication.

**Changes:**
Modified codex-rs/tui/src/bottom_pane/chat_composer.rs: refactored early
return block to continue to normal flow instead of returning immediately
Added test: custom_prompt_with_large_paste_expands_correctly to verify
the fix

**Code Quality:**
No lint warnings
Code follows existing patterns and reuses existing logic
Atomic change focused on the bug fix
This commit is contained in:
Priyadarshini Tamilselvan 2025-11-24 15:18:32 -05:00 committed by GitHub
parent 3741f387e9
commit 523dabc1ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -954,21 +954,17 @@ impl ChatComposer {
return (InputResult::None, true);
}
}
// If we have pending placeholder pastes, submit immediately to expand them.
// If we have pending placeholder pastes, replace them in the textarea text
// and continue to the normal submission flow to handle slash commands.
if !self.pending_pastes.is_empty() {
let mut text = self.textarea.text().to_string();
self.textarea.set_text("");
for (placeholder, actual) in &self.pending_pastes {
if text.contains(placeholder) {
text = text.replace(placeholder, actual);
}
}
self.textarea.set_text(&text);
self.pending_pastes.clear();
if text.is_empty() {
return (InputResult::None, true);
}
self.history.record_local_submission(&text);
return (InputResult::Submitted(text), true);
}
// During a paste-like burst, treat Enter as a newline instead of submit.
@ -3030,6 +3026,70 @@ mod tests {
assert!(composer.textarea.is_empty());
}
#[test]
fn custom_prompt_with_large_paste_expands_correctly() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Create a custom prompt with positional args (no named args like $USER)
composer.set_custom_prompts(vec![CustomPrompt {
name: "code-review".to_string(),
path: "/tmp/code-review.md".to_string().into(),
content: "Please review the following code:\n\n$1".to_string(),
description: None,
argument_hint: None,
}]);
// Type the slash command
let command_text = "/prompts:code-review ";
composer.textarea.set_text(command_text);
composer.textarea.set_cursor(command_text.len());
// Paste large content (>3000 chars) to trigger placeholder
let large_content = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 3000);
composer.handle_paste(large_content.clone());
// Verify placeholder was created
let placeholder = format!("[Pasted Content {} chars]", large_content.chars().count());
assert_eq!(
composer.textarea.text(),
format!("/prompts:code-review {}", placeholder)
);
assert_eq!(composer.pending_pastes.len(), 1);
assert_eq!(composer.pending_pastes[0].0, placeholder);
assert_eq!(composer.pending_pastes[0].1, large_content);
// Submit by pressing Enter
let (result, _needs_redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
// Verify the custom prompt was expanded with the large content as positional arg
match result {
InputResult::Submitted(text) => {
// The prompt should be expanded, with the large content replacing $1
assert_eq!(
text,
format!("Please review the following code:\n\n{}", large_content),
"Expected prompt expansion with large content as $1"
);
}
_ => panic!("expected Submitted, got: {result:?}"),
}
assert!(composer.textarea.is_empty());
assert!(composer.pending_pastes.is_empty());
}
#[test]
fn slash_path_input_submits_without_command_error() {
use crossterm::event::KeyCode;