1use std::path::PathBuf;
2
3use agent_client_protocol as acp;
4
5pub const CONFIG_KEY_MODEL: &str = "model";
7
8#[derive(Debug, Clone)]
10pub struct AvailableMode {
11 pub id: String,
12 pub name: String,
13}
14
15#[derive(Debug)]
20pub struct SessionContext {
21 pub id: Option<acp::SessionId>,
22 pub cwd: PathBuf,
23 available_modes: Vec<AvailableMode>,
24 config_options: Vec<acp::SessionConfigOption>,
25 context_usage_pct: Option<f64>,
26 current_mode_id: Option<String>,
27 cached_model: Option<String>,
28}
29
30impl SessionContext {
31 pub fn new(cwd: PathBuf) -> Self {
32 Self {
33 id: None,
34 available_modes: Vec::new(),
35 config_options: Vec::new(),
36 cwd,
37 context_usage_pct: None,
38 current_mode_id: None,
39 cached_model: None,
40 }
41 }
42
43 pub fn set_session_id(&mut self, session_id: acp::SessionId) {
45 self.id = Some(session_id);
46 }
47
48 pub fn available_modes(&self) -> &[AvailableMode] {
50 &self.available_modes
51 }
52
53 pub fn current_mode_id(&self) -> Option<&str> {
55 self.current_mode_id.as_deref()
56 }
57
58 pub fn set_current_mode_id(&mut self, mode_id: String) {
60 self.current_mode_id = Some(mode_id);
61 }
62
63 pub fn context_usage_pct(&self) -> Option<f64> {
65 self.context_usage_pct
66 }
67
68 pub fn set_context_usage_pct(&mut self, pct: f64) {
70 self.context_usage_pct = Some(pct);
71 }
72
73 pub fn set_modes(&mut self, modes: &acp::SessionModeState) {
75 self.current_mode_id = Some(modes.current_mode_id.to_string());
76 self.available_modes = modes
77 .available_modes
78 .iter()
79 .map(|m| AvailableMode {
80 id: m.id.to_string(),
81 name: m.name.clone(),
82 })
83 .collect();
84 }
85
86 pub fn set_config_options(&mut self, options: Vec<acp::SessionConfigOption>) {
88 self.config_options = options;
89 self.cached_model = self.compute_current_model();
90 }
91
92 pub fn set_optimistic_model(&mut self, model: String) {
95 self.cached_model = Some(model);
96 }
97
98 pub fn current_model(&self) -> Option<&str> {
100 self.cached_model.as_deref()
101 }
102
103 fn compute_current_model(&self) -> Option<String> {
105 self.config_options.iter().find_map(|opt| {
106 if opt.id.to_string() == CONFIG_KEY_MODEL {
107 if let acp::SessionConfigKind::Select(ref select) = opt.kind {
108 return Some(select.current_value.to_string());
109 }
110 }
111 None
112 })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn new_session_has_no_id() {
122 let ctx = SessionContext::new(PathBuf::from("/tmp"));
123 assert!(ctx.id.is_none());
124 assert!(ctx.available_modes.is_empty());
125 assert!(ctx.config_options.is_empty());
126 assert!(ctx.context_usage_pct.is_none());
127 assert!(ctx.current_mode_id.is_none());
128 assert!(ctx.current_model().is_none());
129 }
130
131 #[test]
132 fn set_session_id_stores_id() {
133 let mut ctx = SessionContext::new(PathBuf::from("/tmp"));
134 let id = acp::SessionId::from("test-session-123".to_string());
135 ctx.set_session_id(id);
136 assert!(ctx.id.is_some());
137 assert_eq!(ctx.id.unwrap().to_string(), "test-session-123");
138 }
139
140 #[test]
141 fn current_model_returns_none_when_no_config() {
142 let ctx = SessionContext::new(PathBuf::from("/tmp"));
143 assert!(ctx.current_model().is_none());
144 }
145
146 #[test]
147 fn current_model_returns_value_from_config_options() {
148 let mut ctx = SessionContext::new(PathBuf::from("/tmp"));
149 let option = acp::SessionConfigOption::select(
150 "model",
151 "Model",
152 "claude-sonnet-4-6",
153 vec![acp::SessionConfigSelectOption::new(
154 "claude-sonnet-4-6",
155 "Claude Sonnet 4.6",
156 )],
157 );
158 ctx.set_config_options(vec![option]);
159 assert_eq!(ctx.current_model(), Some("claude-sonnet-4-6"));
160 }
161
162 #[test]
163 fn set_modes_populates_available_modes() {
164 let mut ctx = SessionContext::new(PathBuf::from("/tmp"));
165 let modes = acp::SessionModeState::new(
166 "code",
167 vec![
168 acp::SessionMode::new("code", "Code"),
169 acp::SessionMode::new("chat", "Chat"),
170 ],
171 );
172 ctx.set_modes(&modes);
173 assert_eq!(ctx.current_mode_id.as_deref(), Some("code"));
174 assert_eq!(ctx.available_modes.len(), 2);
175 assert_eq!(ctx.available_modes[0].id, "code");
176 assert_eq!(ctx.available_modes[0].name, "Code");
177 assert_eq!(ctx.available_modes[1].id, "chat");
178 assert_eq!(ctx.available_modes[1].name, "Chat");
179 }
180
181 #[test]
182 fn set_optimistic_model_updates_cached_value() {
183 let mut ctx = SessionContext::new(PathBuf::from("/tmp"));
184 assert!(ctx.current_model().is_none());
185 ctx.set_optimistic_model("claude-sonnet-4-6".to_string());
186 assert_eq!(ctx.current_model(), Some("claude-sonnet-4-6"));
187 }
188
189 #[test]
190 fn set_config_options_caches_model() {
191 let mut ctx = SessionContext::new(PathBuf::from("/tmp"));
192 let option = acp::SessionConfigOption::select(
193 "model",
194 "Model",
195 "claude-opus-4",
196 vec![
197 acp::SessionConfigSelectOption::new("claude-opus-4", "Claude Opus 4"),
198 acp::SessionConfigSelectOption::new("claude-sonnet-4-6", "Claude Sonnet 4.6"),
199 ],
200 );
201 ctx.set_config_options(vec![option]);
202
203 assert_eq!(ctx.current_model(), Some("claude-opus-4"));
205
206 let other = acp::SessionConfigOption::select(
208 "thought_level",
209 "Thought Level",
210 "high",
211 vec![acp::SessionConfigSelectOption::new("high", "High")],
212 );
213 ctx.set_config_options(vec![other]);
214 assert!(ctx.current_model().is_none());
215 }
216}