core-agent-ide/codex-rs/protocol/src/user_input.rs
charley-oai 531748a080
Prompt Expansion: Preserve Text Elements (#9518)
Summary
- Preserve `text_elements` through custom prompt argument parsing and
expansion (named and numeric placeholders).
- Translate text element ranges through Shlex parsing using sentinel
substitution, and rehydrate text + element ranges per arg.
- Drop image attachments when their placeholder does not survive prompt
expansion, keeping attachments consistent with rendered elements.
- Mirror changes in TUI2 and expand tests for prompt parsing/expansion
edge cases.

Tests
- placeholders with spaces as single tokens (positional + key=value,
quoted + unquoted),
  - prompt expansion with image placeholders,
  - large paste + image arg combinations,
  - unused image arg dropped after expansion.
2026-01-20 18:30:20 -08:00

101 lines
3.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
/// User input
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserInput {
Text {
text: String,
/// UI-defined spans within `text` that should be treated as special elements.
/// These are byte ranges into the UTF-8 `text` buffer and are used to render
/// or persist rich input markers (e.g., image placeholders) across history
/// and resume without mutating the literal text.
#[serde(default)]
text_elements: Vec<TextElement>,
},
/// Preencoded data: URI image.
Image { image_url: String },
/// Local image path provided by the user. This will be converted to an
/// `Image` variant (base64 data URL) during request serialization.
LocalImage { path: std::path::PathBuf },
/// Skill selected by the user (name + path to SKILL.md).
Skill {
name: String,
path: std::path::PathBuf,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]
pub struct TextElement {
/// Byte range in the parent `text` buffer that this element occupies.
pub byte_range: ByteRange,
/// Optional human-readable placeholder for the element, displayed in the UI.
placeholder: Option<String>,
}
impl TextElement {
pub fn new(byte_range: ByteRange, placeholder: Option<String>) -> Self {
Self {
byte_range,
placeholder,
}
}
/// Returns a copy of this element with a remapped byte range.
///
/// The placeholder is preserved as-is; callers must ensure the new range
/// still refers to the same logical element (and same placeholder)
/// within the new text.
pub fn map_range<F>(&self, map: F) -> Self
where
F: FnOnce(ByteRange) -> ByteRange,
{
Self {
byte_range: map(self.byte_range),
placeholder: self.placeholder.clone(),
}
}
pub fn set_placeholder(&mut self, placeholder: Option<String>) {
self.placeholder = placeholder;
}
/// Returns the stored placeholder without falling back to the text buffer.
///
/// This must only be used inside `From<TextElement>` implementations on equivalent
/// protocol types where the source text is unavailable. Prefer `placeholder(text)`
/// everywhere else.
#[doc(hidden)]
pub fn _placeholder_for_conversion_only(&self) -> Option<&str> {
self.placeholder.as_deref()
}
pub fn placeholder<'a>(&'a self, text: &'a str) -> Option<&'a str> {
self.placeholder
.as_deref()
.or_else(|| text.get(self.byte_range.start..self.byte_range.end))
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
pub struct ByteRange {
/// Start byte offset (inclusive) within the UTF-8 text buffer.
pub start: usize,
/// End byte offset (exclusive) within the UTF-8 text buffer.
pub end: usize,
}
impl From<std::ops::Range<usize>> for ByteRange {
fn from(range: std::ops::Range<usize>) -> Self {
Self {
start: range.start,
end: range.end,
}
}
}