mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
fix
This commit is contained in:
@@ -25,6 +25,7 @@ use super::command_popup::CommandItem;
|
||||
use super::command_popup::CommandPopup;
|
||||
use super::file_search_popup::FileSearchPopup;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -100,6 +101,7 @@ pub(crate) struct ChatComposer {
|
||||
// Buffer to accumulate characters during a detected non-bracketed paste burst.
|
||||
paste_burst_buffer: String,
|
||||
in_paste_burst_mode: bool,
|
||||
custom_prompts: Vec<CustomPrompt>,
|
||||
}
|
||||
|
||||
/// Popup state – at most one can be visible at any time.
|
||||
@@ -139,6 +141,7 @@ impl ChatComposer {
|
||||
paste_burst_until: None,
|
||||
paste_burst_buffer: String::new(),
|
||||
in_paste_burst_mode: false,
|
||||
custom_prompts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,7 +1107,7 @@ impl ChatComposer {
|
||||
}
|
||||
_ => {
|
||||
if input_starts_with_slash {
|
||||
let mut command_popup = CommandPopup::new();
|
||||
let mut command_popup = CommandPopup::new(self.custom_prompts.clone());
|
||||
command_popup.on_composer_text_change(first_line.to_string());
|
||||
self.active_popup = ActivePopup::Command(command_popup);
|
||||
}
|
||||
@@ -1112,6 +1115,14 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_custom_prompts(&mut self, mut prompts: Vec<CustomPrompt>) {
|
||||
prompts.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
self.custom_prompts = prompts.clone();
|
||||
if let ActivePopup::Command(popup) = &mut self.active_popup {
|
||||
popup.set_prompts(prompts);
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronize `self.file_search_popup` with the current text in the textarea.
|
||||
/// Note this is only called when self.active_popup is NOT Command.
|
||||
fn sync_file_search_popup(&mut self) {
|
||||
@@ -1982,21 +1993,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn selecting_custom_prompt_submits_file_contents() {
|
||||
let tmp = tempdir().expect("create TempDir");
|
||||
let home = tmp.path();
|
||||
let prompts_dir = home.join(".codex").join("prompts");
|
||||
std::fs::create_dir_all(&prompts_dir).expect("mkdir -p ~/.codex/prompts");
|
||||
let prompt_path = prompts_dir.join("my-prompt");
|
||||
let prompt_text = "Hello from saved prompt";
|
||||
std::fs::write(&prompt_path, prompt_text).unwrap();
|
||||
|
||||
unsafe { std::env::set_var("HOME", home) };
|
||||
|
||||
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());
|
||||
|
||||
// Inject prompts as if received via event.
|
||||
composer.set_custom_prompts(vec![CustomPrompt {
|
||||
name: "my-prompt".to_string(),
|
||||
content: prompt_text.to_string(),
|
||||
}]);
|
||||
|
||||
// Type the prompt name to focus it in the slash popup and press Enter.
|
||||
for ch in ['/', 'm', 'y', '-', 'p', 'r', 'o', 'm', 'p', 't'] {
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
|
||||
@@ -9,11 +9,7 @@ use super::selection_popup_common::render_rows;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::built_in_slash_commands;
|
||||
use codex_common::fuzzy_match::fuzzy_match;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PromptEntry {
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
}
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
|
||||
/// A selectable item in the popup: either a built-in command or a user prompt.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -26,24 +22,18 @@ pub(crate) enum CommandItem {
|
||||
pub(crate) struct CommandPopup {
|
||||
command_filter: String,
|
||||
builtins: Vec<(&'static str, SlashCommand)>,
|
||||
prompts: Vec<PromptEntry>,
|
||||
prompts: Vec<CustomPrompt>,
|
||||
state: ScrollState,
|
||||
}
|
||||
|
||||
impl CommandPopup {
|
||||
pub(crate) fn new() -> Self {
|
||||
pub(crate) fn new(mut prompts: Vec<CustomPrompt>) -> Self {
|
||||
let builtins = built_in_slash_commands();
|
||||
let mut exclude = std::collections::HashSet::new();
|
||||
for (name, _) in builtins.iter() {
|
||||
exclude.insert((*name).to_string());
|
||||
}
|
||||
let prompts = codex_core::custom_prompts::discover_prompts_excluding(&exclude)
|
||||
.into_iter()
|
||||
.map(|p| PromptEntry {
|
||||
name: p.name,
|
||||
content: p.content,
|
||||
})
|
||||
.collect();
|
||||
// Exclude prompts that collide with builtin command names and sort by name.
|
||||
let exclude: std::collections::HashSet<String> =
|
||||
builtins.iter().map(|(n, _)| (*n).to_string()).collect();
|
||||
prompts.retain(|p| !exclude.contains(&p.name));
|
||||
prompts.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Self {
|
||||
command_filter: String::new(),
|
||||
builtins,
|
||||
@@ -52,6 +42,17 @@ impl CommandPopup {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_prompts(&mut self, mut prompts: Vec<CustomPrompt>) {
|
||||
let exclude: std::collections::HashSet<String> = self
|
||||
.builtins
|
||||
.iter()
|
||||
.map(|(n, _)| (*n).to_string())
|
||||
.collect();
|
||||
prompts.retain(|p| !exclude.contains(&p.name));
|
||||
prompts.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
self.prompts = prompts;
|
||||
}
|
||||
|
||||
pub(crate) fn prompt_name(&self, idx: usize) -> Option<&str> {
|
||||
self.prompts.get(idx).map(|p| p.name.as_str())
|
||||
}
|
||||
@@ -198,11 +199,10 @@ impl WidgetRef for CommandPopup {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn filter_includes_init_when_typing_prefix() {
|
||||
let mut popup = CommandPopup::new();
|
||||
let mut popup = CommandPopup::new(Vec::new());
|
||||
// Simulate the composer line starting with '/in' so the popup filters
|
||||
// matching commands by prefix.
|
||||
popup.on_composer_text_change("/in".to_string());
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn selecting_init_by_exact_match() {
|
||||
let mut popup = CommandPopup::new();
|
||||
let mut popup = CommandPopup::new(Vec::new());
|
||||
popup.on_composer_text_change("/init".to_string());
|
||||
|
||||
// When an exact match exists, the selected command should be that
|
||||
@@ -237,17 +237,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prompt_discovery_lists_custom_prompts() {
|
||||
let tmp = tempdir().expect("create TempDir");
|
||||
let home = tmp.path();
|
||||
let prompts_dir = home.join(".codex").join("prompts");
|
||||
std::fs::create_dir_all(&prompts_dir).expect("mkdir -p ~/.codex/prompts");
|
||||
std::fs::write(prompts_dir.join("foo"), b"hello from foo").unwrap();
|
||||
std::fs::write(prompts_dir.join("bar"), b"hello from bar").unwrap();
|
||||
|
||||
// Point HOME to the temp dir so discovery uses our fixtures.
|
||||
unsafe { std::env::set_var("HOME", home) };
|
||||
|
||||
let popup = CommandPopup::new();
|
||||
let prompts = vec![
|
||||
CustomPrompt {
|
||||
name: "foo".to_string(),
|
||||
content: "hello from foo".to_string(),
|
||||
},
|
||||
CustomPrompt {
|
||||
name: "bar".to_string(),
|
||||
content: "hello from bar".to_string(),
|
||||
},
|
||||
];
|
||||
let popup = CommandPopup::new(prompts);
|
||||
let items = popup.filtered_items();
|
||||
let mut prompt_names: Vec<String> = items
|
||||
.into_iter()
|
||||
@@ -262,16 +262,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prompt_name_collision_with_builtin_is_ignored() {
|
||||
let tmp = tempdir().expect("create TempDir");
|
||||
let home = tmp.path();
|
||||
let prompts_dir = home.join(".codex").join("prompts");
|
||||
std::fs::create_dir_all(&prompts_dir).expect("mkdir -p ~/.codex/prompts");
|
||||
// Create a prompt with the same name as a builtin command (e.g. "init").
|
||||
std::fs::write(prompts_dir.join("init"), b"should be ignored").unwrap();
|
||||
|
||||
unsafe { std::env::set_var("HOME", home) };
|
||||
|
||||
let popup = CommandPopup::new();
|
||||
// Create a prompt named like a builtin (e.g. "init").
|
||||
let popup = CommandPopup::new(vec![CustomPrompt {
|
||||
name: "init".to_string(),
|
||||
content: "should be ignored".to_string(),
|
||||
}]);
|
||||
let items = popup.filtered_items();
|
||||
let has_collision_prompt = items.into_iter().any(|it| match it {
|
||||
CommandItem::Prompt(i) => popup.prompt_name(i) == Some("init"),
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(crate) enum CancellationEvent {
|
||||
|
||||
pub(crate) use chat_composer::ChatComposer;
|
||||
pub(crate) use chat_composer::InputResult;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
use approval_modal_view::ApprovalModalView;
|
||||
@@ -329,6 +330,12 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Update custom prompts available for the slash popup.
|
||||
pub(crate) fn set_custom_prompts(&mut self, prompts: Vec<CustomPrompt>) {
|
||||
self.composer.set_custom_prompts(prompts);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn composer_is_empty(&self) -> bool {
|
||||
self.composer.is_empty()
|
||||
}
|
||||
|
||||
@@ -1169,6 +1169,9 @@ impl ChatWidget {
|
||||
// Cache prompts locally; UI surfaces them from this store.
|
||||
self.custom_prompts = ev.custom_prompts;
|
||||
debug!("received {} custom prompts", self.custom_prompts.len());
|
||||
// Forward to bottom pane so the slash popup can show them now.
|
||||
self.bottom_pane
|
||||
.set_custom_prompts(self.custom_prompts.clone());
|
||||
}
|
||||
|
||||
/// Programmatically submit a user text message as if typed in the
|
||||
|
||||
Reference in New Issue
Block a user