codex_context_fragments/
fragment.rs1use codex_protocol::models::ContentItem;
2use codex_protocol::models::ResponseInputItem;
3use codex_protocol::models::ResponseItem;
4
5pub trait FragmentRegistration: Sync {
10 fn matches_text(&self, text: &str) -> bool;
11}
12
13pub struct FragmentRegistrationProxy<T> {
14 _marker: std::marker::PhantomData<fn() -> T>,
15}
16
17impl<T> FragmentRegistrationProxy<T> {
18 pub const fn new() -> Self {
19 Self {
20 _marker: std::marker::PhantomData,
21 }
22 }
23}
24
25impl<T> Default for FragmentRegistrationProxy<T> {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationProxy<T> {
32 fn matches_text(&self, text: &str) -> bool {
33 T::matches_text(text)
34 }
35}
36
37pub trait ContextualUserFragment {
47 fn role(&self) -> &'static str;
48
49 fn markers(&self) -> (&'static str, &'static str);
50
51 fn body(&self) -> String;
52
53 fn type_markers() -> (&'static str, &'static str)
54 where
55 Self: Sized;
56
57 fn matches_text(text: &str) -> bool
58 where
59 Self: Sized,
60 {
61 let (start_marker, end_marker) = Self::type_markers();
62 matches_marked_text(start_marker, end_marker, text)
63 }
64
65 fn render(&self) -> String {
66 let (start_marker, end_marker) = self.markers();
67 let body = self.body();
68 if start_marker.is_empty() && end_marker.is_empty() {
69 return body;
70 }
71
72 format!("{start_marker}{body}{end_marker}")
73 }
74
75 fn into(self) -> ResponseItem
76 where
77 Self: Sized,
78 {
79 ResponseItem::Message {
80 id: None,
81 role: self.role().to_string(),
82 content: vec![ContentItem::InputText {
83 text: self.render(),
84 }],
85 phase: None,
86 internal_chat_message_metadata_passthrough: None,
87 }
88 }
89
90 fn into_boxed_response_item(self: Box<Self>) -> ResponseItem {
91 ResponseItem::Message {
92 id: None,
93 role: self.role().to_string(),
94 content: vec![ContentItem::InputText {
95 text: self.render(),
96 }],
97 phase: None,
98 internal_chat_message_metadata_passthrough: None,
99 }
100 }
101
102 fn into_response_input_item(self) -> ResponseInputItem
103 where
104 Self: Sized,
105 {
106 ResponseInputItem::Message {
107 role: self.role().to_string(),
108 content: vec![ContentItem::InputText {
109 text: self.render(),
110 }],
111 phase: None,
112 }
113 }
114}
115
116pub(crate) fn matches_marked_text(start_marker: &str, end_marker: &str, text: &str) -> bool {
117 if start_marker.is_empty() || end_marker.is_empty() {
118 return false;
119 }
120
121 let trimmed = text.trim_start();
122 let starts_with_marker = trimmed
123 .get(..start_marker.len())
124 .is_some_and(|candidate| candidate.eq_ignore_ascii_case(start_marker));
125 let trimmed = trimmed.trim_end();
126 let ends_with_marker = trimmed
127 .get(trimmed.len().saturating_sub(end_marker.len())..)
128 .is_some_and(|candidate| candidate.eq_ignore_ascii_case(end_marker));
129 starts_with_marker && ends_with_marker
130}