tokn_headers/schemas/overlays/
copilot.rs1use crate::error::Error;
14use crate::keys;
15use crate::map::HeaderMap;
16use crate::name::HeaderName;
17use crate::schema::{from_inbound_or, opt_from_inbound, optional, put, put_opt, required, HeaderSchema};
18use crate::vars::TemplateVars;
19use serde::{Deserialize, Serialize};
20use smol_str::SmolStr;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct CopilotOverlay {
24 #[serde(rename = "Editor-Version")]
25 pub editor_version: SmolStr,
26 #[serde(rename = "Editor-Plugin-Version")]
27 pub editor_plugin_version: SmolStr,
28 #[serde(rename = "Copilot-Integration-Id")]
29 pub integration_id: SmolStr,
30 #[serde(rename = "Copilot-Vision-Request")]
31 pub vision_request: Option<SmolStr>,
32 #[serde(rename = "X-Initiator")]
33 pub initiator: Option<SmolStr>,
34}
35
36impl HeaderSchema for CopilotOverlay {
37 fn parse(map: &HeaderMap) -> Result<Self, Error> {
38 Ok(Self {
39 editor_version: required(map, &keys::EDITOR_VERSION)?,
40 editor_plugin_version: required(map, &keys::EDITOR_PLUGIN_VERSION)?,
41 integration_id: required(map, &keys::COPILOT_INTEGRATION_ID)?,
42 vision_request: optional(map, &keys::COPILOT_VISION_REQUEST),
43 initiator: optional(map, &keys::X_INITIATOR),
44 })
45 }
46 fn dump(&self) -> HeaderMap {
47 let mut m = HeaderMap::new();
48 put(&mut m, &keys::EDITOR_VERSION, &self.editor_version);
49 put(&mut m, &keys::EDITOR_PLUGIN_VERSION, &self.editor_plugin_version);
50 put(&mut m, &keys::COPILOT_INTEGRATION_ID, &self.integration_id);
51 put_opt(&mut m, &keys::COPILOT_VISION_REQUEST, &self.vision_request);
52 put_opt(&mut m, &keys::X_INITIATOR, &self.initiator);
53 m
54 }
55 fn known_names() -> &'static [&'static HeaderName] {
56 static NAMES: [&HeaderName; 5] = [
57 &keys::EDITOR_VERSION,
58 &keys::EDITOR_PLUGIN_VERSION,
59 &keys::COPILOT_INTEGRATION_ID,
60 &keys::COPILOT_VISION_REQUEST,
61 &keys::X_INITIATOR,
62 ];
63 &NAMES
64 }
65}
66
67impl CopilotOverlay {
68 pub fn build(_vars: &TemplateVars, inbound: &HeaderMap) -> Self {
72 Self {
73 editor_version: from_inbound_or(inbound, &keys::EDITOR_VERSION, || "vscode/1.95.0".into()),
74 editor_plugin_version: from_inbound_or(inbound, &keys::EDITOR_PLUGIN_VERSION, || "copilot-chat/0.23.0".into()),
75 integration_id: from_inbound_or(inbound, &keys::COPILOT_INTEGRATION_ID, || "vscode-chat".into()),
76 vision_request: opt_from_inbound(inbound, &keys::COPILOT_VISION_REQUEST),
77 initiator: opt_from_inbound(inbound, &keys::X_INITIATOR),
78 }
79 }
80
81 pub fn apply_to(&self, map: &mut HeaderMap, _vars: &TemplateVars) {
87 map.insert(&keys::EDITOR_VERSION, self.editor_version.to_string());
88 map.insert(&keys::EDITOR_PLUGIN_VERSION, self.editor_plugin_version.to_string());
89 map.insert(&keys::COPILOT_INTEGRATION_ID, self.integration_id.to_string());
90 if let Some(v) = &self.vision_request {
91 if !map.contains_key(&keys::COPILOT_VISION_REQUEST) {
92 map.insert(&keys::COPILOT_VISION_REQUEST, v.to_string());
93 }
94 }
95 if let Some(v) = &self.initiator {
96 if !map.contains_key(&keys::X_INITIATOR) {
97 map.insert(&keys::X_INITIATOR, v.to_string());
98 }
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn round_trip() {
109 let h = CopilotOverlay {
110 editor_version: "vscode/1.95.0".into(),
111 editor_plugin_version: "copilot-chat/0.23.0".into(),
112 integration_id: "vscode-chat".into(),
113 vision_request: Some("true".into()),
114 initiator: Some("agent".into()),
115 };
116 assert_eq!(CopilotOverlay::parse(&h.dump()).unwrap(), h);
117 }
118
119 #[test]
120 fn missing_required_errors() {
121 let m = HeaderMap::new();
122 assert!(matches!(CopilotOverlay::parse(&m), Err(Error::MissingHeader { .. })));
123 }
124
125 #[test]
126 fn build_with_empty_inbound_uses_defaults() {
127 let h = CopilotOverlay::build(&TemplateVars::default(), &HeaderMap::new());
128 assert_eq!(h.editor_version.as_str(), "vscode/1.95.0");
129 assert_eq!(h.editor_plugin_version.as_str(), "copilot-chat/0.23.0");
130 assert_eq!(h.integration_id.as_str(), "vscode-chat");
131 assert!(h.vision_request.is_none());
132 assert!(h.initiator.is_none());
133 }
134
135 #[test]
136 fn build_passes_through_inbound() {
137 let mut inbound = HeaderMap::new();
138 inbound.insert(&keys::EDITOR_VERSION, "vscode/1.99.0");
139 inbound.insert(&keys::COPILOT_VISION_REQUEST, "true");
140 let h = CopilotOverlay::build(&TemplateVars::default(), &inbound);
141 assert_eq!(h.editor_version.as_str(), "vscode/1.99.0");
142 assert_eq!(h.vision_request.as_deref(), Some("true"));
143 }
144
145 #[test]
146 fn build_uses_vars_for_correlation() {
147 let vars = TemplateVars {
150 session_id: Some("ses_xyz".into()),
151 ..Default::default()
152 };
153 let h = CopilotOverlay::build(&vars, &HeaderMap::new());
154 assert_eq!(h.integration_id.as_str(), "vscode-chat");
155 }
156
157 #[test]
158 fn apply_to_overrides_managed_fields_and_skips_optionals_when_none() {
159 let mut map = HeaderMap::new();
161 map.insert(&keys::EDITOR_VERSION, "stale/0.0.0");
162 map.insert(&keys::COPILOT_INTEGRATION_ID, "old-integration");
163 map.insert(&keys::X_INITIATOR, "preexisting");
164
165 let overlay = CopilotOverlay {
166 editor_version: "vscode/1.95.0".into(),
167 editor_plugin_version: "copilot-chat/0.23.0".into(),
168 integration_id: "vscode-chat".into(),
169 vision_request: None,
170 initiator: Some("agent".into()),
171 };
172 overlay.apply_to(&mut map, &TemplateVars::default());
173
174 assert_eq!(map.get(&keys::EDITOR_VERSION).unwrap().as_str(), "vscode/1.95.0");
176 assert_eq!(map.get(&keys::COPILOT_INTEGRATION_ID).unwrap().as_str(), "vscode-chat");
177 assert_eq!(
178 map.get(&keys::EDITOR_PLUGIN_VERSION).unwrap().as_str(),
179 "copilot-chat/0.23.0"
180 );
181 assert_eq!(map.get(&keys::X_INITIATOR).unwrap().as_str(), "preexisting");
183 assert!(!map.contains_key(&keys::COPILOT_VISION_REQUEST));
185 }
186}