Files
codex/prs/bolinfest/study/PR-2696-study.md
2025-09-02 15:17:45 -07:00

5.8 KiB
Raw Blame History

DOs

  • Bold: Prefer async I/O with tokio::fs: Use async directory iteration and reads; return empty on errors.
use tokio::fs;
use std::path::Path;
use codex_protocol::custom_prompts::CustomPrompt;

pub async fn discover_prompts_in(dir: &Path) -> Vec<CustomPrompt> {
    let mut out = Vec::new();
    let mut entries = match fs::read_dir(dir).await {
        Ok(e) => e,
        Err(_) => return out,
    };
    while let Ok(Some(entry)) = entries.next_entry().await {
        let path = entry.path();
        if !entry.file_type().await.map(|t| t.is_file()).unwrap_or(false) { continue; }
        if !path.extension().and_then(|s| s.to_str()).map(|e| e.eq_ignore_ascii_case("md")).unwrap_or(false) { continue; }
        let Some(name) = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()) else { continue; };
        let content = match fs::read_to_string(&path).await { Ok(s) => s, Err(_) => continue };
        out.push(CustomPrompt { name, path, content });
    }
    out.sort_by(|a, b| a.name.cmp(&b.name));
    out
}
  • Bold: Use PathBuf for file paths: Make it easy to open/edit files later.
use serde::{Serialize, Deserialize};
use std::path::PathBuf;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CustomPrompt {
    pub name: String,
    pub path: PathBuf,
    pub content: String,
}
  • Bold: Provide a clear default prompts dir: Resolve $CODEX_HOME/prompts safely.
use std::path::PathBuf;

pub fn default_prompts_dir() -> Option<PathBuf> {
    crate::config::find_codex_home().ok().map(|home| home.join("prompts"))
}
  • Bold: Import protocol types and use them directly: Keep event construction readable.
use crate::protocol::{Event, EventMsg, ListCustomPromptsResponseEvent, Op};

let event = Event {
    id: sub_id,
    msg: EventMsg::ListCustomPromptsResponse(ListCustomPromptsResponseEvent { custom_prompts }),
};
  • Bold: Exclude name collisions with built-ins: Filter user prompts against builtin command names.
use std::collections::HashSet;

let exclude: 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));
  • Bold: Name enum variants precisely: Prefer UserPrompt over generic names like Prompt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CommandItem {
    Builtin(SlashCommand),
    UserPrompt(usize), // index into prompts vec
}
  • Bold: Sort filtered items efficiently: Sort by score, then by name; compute names only if needed.
out.sort_by(|a, b| {
    a.2.cmp(&b.2).then_with(|| {
        let an = match a.0 { CommandItem::Builtin(c) => c.command(), CommandItem::UserPrompt(i) => &self.prompts[i].name };
        let bn = match b.0 { CommandItem::Builtin(c) => c.command(), CommandItem::UserPrompt(i) => &self.prompts[i].name };
        an.cmp(bn)
    })
});
  • Bold: Show helpful descriptions for prompts: Consider first line as the description.
let desc = self.prompts[i].content.lines().next().unwrap_or("send saved prompt");
GenericDisplayRow { name: format!("/{}", self.prompts[i].name), description: Some(desc.to_string()), /* ... */ }
  • Bold: Use inline capture in logs/formatting: Favor {var} capture where supported.
let len = ev.custom_prompts.len();
debug!("received {len} custom prompts"); // tracing's inline capture
let cmd = "init";
let s = format!("/{cmd}");               // Rust inline capture in format!
  • Bold: Make tests async-friendly and concise: Use #[tokio::test]; derive PartialEq to assert directly.
#[derive(Debug, PartialEq)]
enum InputResult { Submitted(String), /* ... */ }

#[tokio::test]
async fn discovers_and_sorts_files() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(dir.path().join("b.md"), "b").unwrap();
    std::fs::write(dir.path().join("a.md"), "a").unwrap();
    let names: Vec<_> = discover_prompts_in(dir.path()).await.into_iter().map(|e| e.name).collect();
    assert_eq!(names, vec!["a", "b"]);
}

DONTs

  • Bold: Dont offload light I/O to spawn_blocking: Use async fs instead of a blocking thread pool.
// Anti-pattern
let custom_prompts = tokio::task::spawn_blocking(|| discover_prompts_in(&dir)).await.unwrap_or_default();

// Prefer
let custom_prompts = crate::custom_prompts::discover_prompts_in(&dir).await;
  • Bold: Dont use String for paths: Use PathBuf to preserve platform semantics.
// Avoid
struct CustomPrompt { path: String, /* ... */ }

// Prefer
struct CustomPrompt { path: std::path::PathBuf, /* ... */ }
  • Bold: Dont fully qualify types when imports clarify code: Import once, simplify everywhere.
// Avoid
msg: EventMsg::ListCustomPromptsResponse(crate::protocol::ListCustomPromptsResponseEvent { custom_prompts })

// Prefer
use crate::protocol::ListCustomPromptsResponseEvent;
msg: EventMsg::ListCustomPromptsResponse(ListCustomPromptsResponseEvent { custom_prompts })
  • Bold: Dont leak collisions into the UI: Never show user prompts that shadow built-ins.
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
prompts.retain(|p| !exclude.contains(&p.name)); // required
  • Bold: Dont over-compute during sort: Avoid building names before comparing scores.
// Avoid: compute names first
out.sort_by(|a, b| {
    let an = /* ... */; let bn = /* ... */;
    a.2.cmp(&b.2).then(an.cmp(bn))
});

// Prefer: compute only inside then_with
out.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| /* compute names here */));
  • Bold: Dont write verbose assertions: Use one-line assert_eq! when the type implements PartialEq.
assert_eq!(InputResult::Submitted(prompt_text.to_string()), result);
  • Bold: Dont assume sync tests for async code: Mark async tests with #[tokio::test].
#[tokio::test]
async fn skips_non_utf8_files() { /* ... */ }