1use std::path::Path;
4
5use serde_json::{json, Map, Value};
6
7use crate::clock::Clock;
8use crate::engine::{DiffKey, Engine};
9use crate::error::{ErrorCode, ToolError, ToolResult};
10use crate::transport::Transport;
11
12use super::{SessionChangesArgs, WorkspaceStatusArgs};
13
14const RESERVE: usize = 64;
16const PATCH_OVERHEAD: usize = 12;
18const FLAG_OVERHEAD: usize = 23;
20
21fn serialized_len(v: &Value) -> usize {
22 v.to_string().len()
23}
24
25fn escaped_len(c: char) -> usize {
26 match c {
27 '"' | '\\' | '\n' | '\r' | '\t' | '\u{8}' | '\u{c}' => 2,
28 c if (c as u32) < 0x20 => 6,
29 c => c.len_utf8(),
30 }
31}
32
33fn cut_escaped(s: &str, max: usize) -> String {
35 let mut used = 0;
36 let mut out = String::new();
37 for c in s.chars() {
38 let n = escaped_len(c);
39 if used + n > max {
40 break;
41 }
42 used += n;
43 out.push(c);
44 }
45 out
46}
47
48fn range_error(e: ToolError) -> ToolError {
49 let tag = e
50 .details
51 .as_ref()
52 .and_then(|d| d.get("tag"))
53 .and_then(Value::as_str);
54 if matches!(tag, Some("MessageNotFoundError" | "InvalidRequestError")) {
55 ToolError {
56 code: ErrorCode::InvalidRequest,
57 ..e
58 }
59 } else {
60 e
61 }
62}
63
64fn meta_of(file: &Value) -> Map<String, Value> {
65 let mut m = Map::new();
66 for k in ["file", "status", "additions", "deletions"] {
67 m.insert(k.to_string(), file.get(k).cloned().unwrap_or(Value::Null));
68 }
69 m
70}
71
72fn pack(head: &Map<String, Value>, files: &[Value], patches: bool, max: usize) -> Value {
75 let budget = max.saturating_sub(RESERVE);
76 let metas: Vec<Map<String, Value>> = files.iter().map(meta_of).collect();
77 let meta_cost: Vec<usize> = metas
78 .iter()
79 .map(|m| serialized_len(&Value::Object(m.clone())) + 1)
80 .collect();
81 let name_cost: Vec<usize> = metas
82 .iter()
83 .map(|m| serialized_len(&m["file"]) + 1)
84 .collect();
85 let mut skeleton = head.clone();
86 skeleton.insert("files".into(), json!([]));
87 skeleton.insert("omitted_patches".into(), json!(files.len()));
88 skeleton.insert("omitted_files".into(), json!([]));
89 let base = serialized_len(&Value::Object(skeleton));
90 let mut keep = files.len();
91 let mut total = base + meta_cost.iter().sum::<usize>();
92 while keep > 0 && total > budget {
93 keep -= 1;
94 total = total - meta_cost[keep] + name_cost[keep];
95 }
96 let mut listed: Vec<Value> = Vec::new();
97 let mut with_patch = 0usize;
98 let full = budget.saturating_sub(total);
99 let mut remaining = full;
100 for (i, m) in metas.iter().take(keep).enumerate() {
101 let mut entry = m.clone();
102 if patches {
103 if let Some(p) = files[i].get("patch").and_then(Value::as_str) {
104 let need = serialized_len(&Value::String(p.to_string())) + PATCH_OVERHEAD;
105 if need <= remaining {
106 entry.insert("patch".into(), json!(p));
107 remaining -= need;
108 with_patch += 1;
109 } else if need > full {
110 let room = remaining.saturating_sub(PATCH_OVERHEAD + FLAG_OVERHEAD);
112 if room > 0 {
113 entry.insert("patch".into(), json!(cut_escaped(p, room)));
114 entry.insert("patch_truncated".into(), json!(true));
115 with_patch += 1;
116 remaining = 0;
117 }
118 }
119 }
120 }
121 listed.push(Value::Object(entry));
122 }
123 let omitted_files: Vec<Value> = metas.iter().skip(keep).map(|m| m["file"].clone()).collect();
124 let omitted_patches = if patches {
125 keep - with_patch
126 } else {
127 files.len()
128 };
129 let mut out = head.clone();
130 out.insert("files".into(), Value::Array(listed));
131 out.insert("omitted_patches".into(), json!(omitted_patches));
132 out.insert("omitted_files".into(), Value::Array(omitted_files));
133 Value::Object(out)
134}
135
136impl<T: Transport, C: Clock> Engine<T, C> {
137 async fn first_user_message(&self, id: &str, order: &str) -> Result<Option<String>, ToolError> {
138 let params = vec![
139 ("sessionID".to_string(), id.to_string()),
140 ("type".to_string(), "user".to_string()),
141 ("limit".to_string(), "1".to_string()),
142 ("order".to_string(), order.to_string()),
143 ];
144 let (data, _, _) = self.call_json("session.message.list", params, None).await?;
145 Ok(data
146 .as_array()
147 .and_then(|a| a.first())
148 .and_then(|m| m.get("id"))
149 .and_then(Value::as_str)
150 .map(str::to_string))
151 }
152
153 async fn session_directory(&self, id: &str) -> Result<Value, ToolError> {
154 if self.env.session.as_deref() == Some(id) {
155 if let Some(d) = &self.env.directory {
156 return Ok(json!(d.to_string_lossy()));
157 }
158 }
159 let (s, _, _) = self
160 .call_json(
161 "session.get",
162 vec![("sessionID".to_string(), id.to_string())],
163 None,
164 )
165 .await?;
166 Ok(s.pointer("/location/directory")
167 .cloned()
168 .unwrap_or(Value::Null))
169 }
170
171 pub async fn session_changes(&self, a: SessionChangesArgs) -> ToolResult {
172 let id = self.resolve_session(a.session_id.as_deref())?;
173 let directory = self.session_directory(&id).await?;
174 let from = match a.from {
175 Some(f) => Some(f),
176 None => self.first_user_message(&id, "asc").await?,
177 };
178 let to = match a.to {
179 Some(t) => Some(t),
180 None => self.first_user_message(&id, "desc").await?,
181 };
182 let mut head = Map::new();
183 head.insert("directory".into(), directory);
184 head.insert("from".into(), json!(from));
185 head.insert("to".into(), json!(to));
186 let (Some(from), Some(to)) = (from, to) else {
187 return Ok(pack(&head, &[], true, self.settings.output_max_bytes));
189 };
190 let key: DiffKey = (id.clone(), from.clone(), to.clone(), a.context);
191 let diff = match self.diff_cache_get(&key) {
192 Some(d) => d,
193 None => {
194 let mut params = vec![
195 ("sessionID".to_string(), id),
196 ("from".to_string(), from),
197 ("to".to_string(), to),
198 ];
199 if let Some(c) = a.context {
200 params.push(("context".to_string(), c.to_string()));
201 }
202 let (d, _, _) = self
203 .call_json("session.diff", params, None)
204 .await
205 .map_err(range_error)?;
206 self.diff_cache_put(key, &d);
207 d
208 }
209 };
210 let mut files = diff.as_array().cloned().unwrap_or_default();
211 if let Some(paths) = &a.paths {
212 files.retain(|f| {
213 f.get("file")
214 .and_then(Value::as_str)
215 .is_some_and(|n| paths.iter().any(|p| p == n))
216 });
217 }
218 Ok(pack(
219 &head,
220 &files,
221 a.patches.unwrap_or(true),
222 self.settings.output_max_bytes,
223 ))
224 }
225
226 pub async fn workspace_status(&self, a: WorkspaceStatusArgs) -> ToolResult {
227 let dir = self
228 .resolve_directory(a.directory.as_deref().map(Path::new))
229 .to_string_lossy()
230 .to_string();
231 let params = || vec![("location[directory]".to_string(), dir.clone())];
232 let (info, _, _) = self.call_json("vcs.get", params(), None).await?;
233 let provider = info.get("provider").filter(|p| !p.is_null()).cloned();
234 let Some(provider) = provider else {
235 return Ok(json!({"directory": dir, "vcs": null}));
236 };
237 let (status, _, meta) = self.call_json("vcs.status", params(), None).await?;
238 let entries: Vec<Value> = status
239 .as_array()
240 .map(|a| {
241 a.iter()
242 .map(|e| {
243 json!({
244 "path": e.get("path").cloned().unwrap_or(Value::Null),
245 "kind": e.get("kind").cloned().unwrap_or(Value::Null),
246 })
247 })
248 .collect()
249 })
250 .unwrap_or_default();
251 let mut out = Map::new();
252 out.insert("directory".into(), json!(dir));
253 out.insert("vcs".into(), provider);
254 out.insert("status".into(), Value::Array(entries));
255 if let Some(m) = meta {
256 out.insert("meta".into(), m);
257 }
258 Ok(Value::Object(out))
259 }
260}