1use glob::glob;
2use std::fmt::Write as FmtWrite;
3use std::fs::{self, File};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use tokio::sync::Mutex;
8use tracing::{debug, warn};
9
10use crate::errors::{Result, WinxError};
11use crate::state::bash_state::BashState;
12use crate::types::ContextSave;
13use crate::utils::path::expand_user;
14
15pub async fn handle_tool_call(
29 bash_state: &Arc<Mutex<Option<BashState>>>,
30 args: ContextSave,
31) -> Result<String> {
32 let bash_state_guard = bash_state.lock().await;
34
35 let bash_state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
36
37 let result = save_context(bash_state, args)?;
39
40 if let Err(e) = try_open_file(&result) {
42 debug!("Failed to open the context file: {}", e);
43 }
45
46 Ok(result)
47}
48
49fn save_context(bash_state: &BashState, mut context: ContextSave) -> Result<String> {
60 normalize_context(&mut context)?;
61
62 let (relevant_files, warnings) = collect_relevant_files(&context)?;
63 let memory_dir = resolve_memory_dir()?;
64 let relevant_files_data = read_files_content(&relevant_files, 10_000)?;
65 let memory_data = format_memory(&context, &relevant_files_data);
66 let safe_id = sanitize_filename(&context.id);
67
68 let memory_file_path = memory_dir.join(format!("{safe_id}.txt"));
69 if let Some(response) = write_memory_file(&memory_file_path, &memory_data, &context)? {
70 return Ok(response);
71 }
72
73 let state_file_path = memory_dir.join(format!("{safe_id}_bash_state.json"));
74 write_bash_state_file(&state_file_path, bash_state)?;
75
76 Ok(context_save_response(&relevant_files, &warnings, &context, &memory_file_path))
77}
78
79fn normalize_context(context: &mut ContextSave) -> Result<()> {
80 if !context.project_root_path.is_empty() {
81 context.project_root_path = expand_user(&context.project_root_path);
82 }
83
84 if context.id.is_empty() {
85 return Err(WinxError::ArgumentParseError("Task ID cannot be empty".to_string()));
86 }
87
88 Ok(())
89}
90
91fn collect_relevant_files(context: &ContextSave) -> Result<(Vec<PathBuf>, Vec<String>)> {
92 let mut relevant_files = Vec::new();
93 let mut warnings = Vec::new();
94
95 for glob_pattern in &context.relevant_file_globs {
96 let expanded_glob = expand_user(glob_pattern);
98
99 let final_glob =
101 if !Path::new(&expanded_glob).is_absolute() && !context.project_root_path.is_empty() {
102 PathBuf::from(&context.project_root_path)
103 .join(expanded_glob)
104 .to_string_lossy()
105 .to_string()
106 } else {
107 expanded_glob
108 };
109
110 debug!("Processing glob pattern: {}", final_glob);
111
112 let matches = glob(&final_glob).map_err(|e| {
114 WinxError::ArgumentParseError(format!("Invalid glob pattern '{final_glob}': {e}"))
115 })?;
116
117 let mut found_files = false;
118 for entry in matches {
119 match entry {
120 Ok(path) => {
121 if path.is_file() {
122 relevant_files.push(path);
123 found_files = true;
124 if relevant_files.len() >= 1000 {
126 warn!("Reached limit of 1000 files for glob '{}'", final_glob);
127 break;
128 }
129 }
130 }
131 Err(e) => {
132 warn!("Error matching glob '{}': {}", final_glob, e);
133 }
134 }
135 }
136
137 if !found_files {
138 warnings.push(format!("Warning: No files found for the glob: {glob_pattern}"));
139 }
140 }
141
142 debug!("Found {} relevant files", relevant_files.len());
143 Ok((relevant_files, warnings))
144}
145
146fn resolve_memory_dir() -> Result<PathBuf> {
147 let app_dir = match get_app_dir_xdg() {
148 Ok(dir) => dir,
149 Err(e) => {
150 debug!("Failed to get primary app directory: {:?}", e);
151 let fallback = std::env::temp_dir().join("winx-memory");
153 debug!("Using fallback directory: {}", fallback.display());
154 fs::create_dir_all(&fallback).map_err(|e2| WinxError::FileAccessError {
155 path: fallback.clone(),
156 message: format!(
157 "Failed to create fallback directory: {e2} (after previous error: {e:?})"
158 ),
159 })?;
160 fallback
161 }
162 };
163
164 let mut memory_dir = app_dir.join("memory");
165 match fs::create_dir_all(&memory_dir) {
166 Ok(()) => {}
167 Err(e) => {
168 debug!("Failed to create memory directory: {}", e);
169 debug!("Using app_dir directly as memory_dir due to failed subdirectory creation");
171 memory_dir = app_dir;
172 }
173 }
174
175 Ok(memory_dir)
176}
177
178fn write_memory_file(
179 memory_file_path: &Path,
180 memory_data: &str,
181 context: &ContextSave,
182) -> Result<Option<String>> {
183 match File::create(memory_file_path) {
184 Ok(mut file) => {
185 if let Err(e) = file.write_all(memory_data.as_bytes()) {
186 warn!("Failed to write memory data: {}", e);
187 return Ok(Some(save_to_temp_file(memory_data, context)?));
188 }
189 }
190 Err(e) => {
191 warn!("Failed to create memory file: {}", e);
192 return Ok(Some(save_to_temp_file(memory_data, context)?));
193 }
194 }
195
196 Ok(None)
197}
198
199fn write_bash_state_file(state_file_path: &Path, bash_state: &BashState) -> Result<()> {
200 let bash_state_dict = serde_json::json!({
201 "cwd": bash_state.cwd.to_string_lossy().to_string(),
202 "workspace_root": bash_state.workspace_root.to_string_lossy().to_string(),
203 "mode": match bash_state.mode {
204 crate::types::Modes::Wcgw => "wcgw",
205 crate::types::Modes::Architect => "architect",
206 crate::types::Modes::CodeWriter => "code_writer",
207 }
208 });
209
210 let state_json = serde_json::to_string_pretty(&bash_state_dict).map_err(|e| {
211 WinxError::SerializationError(format!("Failed to serialize bash state: {e}"))
212 })?;
213
214 match File::create(state_file_path) {
216 Ok(mut state_file) => {
217 if let Err(e) = state_file.write_all(state_json.as_bytes()) {
218 warn!("Failed to write bash state data: {}", e);
219 }
221 }
222 Err(e) => {
223 warn!("Failed to create bash state file: {}", e);
224 }
226 }
227
228 Ok(())
229}
230
231fn context_save_response(
232 relevant_files: &[PathBuf],
233 warnings: &[String],
234 context: &ContextSave,
235 memory_file_path: &Path,
236) -> String {
237 let memory_file_path_str = memory_file_path.to_string_lossy().to_string();
238 if !relevant_files.is_empty() || context.relevant_file_globs.is_empty() {
239 if warnings.is_empty() {
240 memory_file_path_str
241 } else {
242 format!(
243 "{}\n\nContext file successfully saved at {}",
244 warnings.join("\n"),
245 memory_file_path_str
246 )
247 }
248 } else {
249 format!(
250 "Error: No files found for the given globs. Context file successfully saved at \"{memory_file_path_str}\", but please fix the error."
251 )
252 }
253}
254
255fn format_memory(context: &ContextSave, relevant_files_data: &str) -> String {
266 let mut memory_data = String::new();
267
268 if !context.project_root_path.is_empty() {
270 let _ = write!(memory_data, "Project root path: {}\n\n", context.project_root_path);
271 }
272
273 memory_data.push_str(&context.description);
275 memory_data.push_str("\n\n");
276
277 let _ =
279 write!(memory_data, "Relevant file globs: {}\n\n", context.relevant_file_globs.join(", "));
280
281 memory_data.push_str("File contents:\n\n");
283 memory_data.push_str(relevant_files_data);
284
285 memory_data
286}
287
288fn get_app_dir_xdg() -> Result<PathBuf> {
300 let app_dir = get_primary_app_dir().or_else(|_| get_fallback_app_dir())?;
302
303 debug!("Using app directory: {}", app_dir.display());
304 Ok(app_dir)
305}
306
307fn get_primary_app_dir() -> Result<PathBuf> {
309 if let Ok(xdg_path) = std::env::var("XDG_DATA_HOME") {
311 let app_dir = PathBuf::from(xdg_path).join("winx");
312 if let Ok(()) = fs::create_dir_all(&app_dir) {
313 return Ok(app_dir);
314 }
315 }
316
317 if let Ok(home) = std::env::var("HOME") {
319 let app_dir = PathBuf::from(home).join(".local/share/winx");
320 if let Ok(()) = fs::create_dir_all(&app_dir) {
321 return Ok(app_dir);
322 }
323 }
324
325 Err(WinxError::FileAccessError {
327 path: PathBuf::from("<primary-paths>"),
328 message: "Could not create app directory in primary locations".to_string(),
329 })
330}
331
332fn get_fallback_app_dir() -> Result<PathBuf> {
334 let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
336
337 let app_dir = current_dir.join(".winx-data");
338 if let Ok(()) = fs::create_dir_all(&app_dir) {
339 return Ok(app_dir);
340 }
341
342 let temp_dir = std::env::temp_dir().join("winx-data");
344 fs::create_dir_all(&temp_dir).map_err(|e| WinxError::FileAccessError {
345 path: temp_dir.clone(),
346 message: format!("Failed to create app directory in any location: {e}"),
347 })?;
348
349 Ok(temp_dir)
350}
351
352fn read_files_content(file_paths: &[PathBuf], max_files: usize) -> Result<String> {
363 let mut result = String::new();
364 let mut skipped_binary = Vec::new();
365
366 for (i, path) in file_paths.iter().take(max_files).enumerate() {
367 match fs::read_to_string(path) {
369 Ok(file_content) => {
370 let _ = writeln!(result, "--- File {}: {} ---", i + 1, path.display());
371 result.push_str(&file_content);
372 result.push_str("\n\n");
373 }
374 Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
375 skipped_binary.push(path.display().to_string());
377 }
378 Err(e) => {
379 return Err(WinxError::FileAccessError {
380 path: path.clone(),
381 message: format!("Failed to read file: {e}"),
382 });
383 }
384 }
385 }
386
387 if !skipped_binary.is_empty() {
389 let _ = write!(
390 result,
391 "Note: Skipped {} binary/non-text file(s): {}\n\n",
392 skipped_binary.len(),
393 skipped_binary.join(", ")
394 );
395 }
396
397 if file_paths.len() > max_files {
398 let _ = writeln!(
399 result,
400 "Note: Only showing the first {} files out of {}.",
401 max_files,
402 file_paths.len()
403 );
404 }
405
406 Ok(result)
407}
408
409fn try_open_file(file_path: &str) -> Result<()> {
419 if std::env::consts::OS != "macos" && std::env::consts::OS != "linux" {
420 return Ok(());
422 }
423
424 let cmd = if std::env::consts::OS == "macos" {
426 "open"
427 } else {
428 for cmd in &["xdg-open", "gnome-open", "kde-open"] {
430 let status = std::process::Command::new("which")
431 .arg(cmd)
432 .stdout(std::process::Stdio::null())
433 .stderr(std::process::Stdio::null())
434 .status();
435
436 if let Ok(status) = status {
437 if status.success() {
438 let _ =
440 std::process::Command::new(cmd).arg(file_path).spawn().map_err(|e| {
441 WinxError::CommandExecutionError(format!(
442 "Failed to spawn open command: {e}"
443 ))
444 })?;
445
446 return Ok(());
448 }
449 }
450 }
451
452 return Ok(());
454 };
455
456 let _ = std::process::Command::new(cmd).arg(file_path).spawn().map_err(|e| {
458 WinxError::CommandExecutionError(format!("Failed to spawn open command: {e}"))
459 })?;
460
461 Ok(())
466}
467
468fn save_to_temp_file(memory_data: &str, context: &ContextSave) -> Result<String> {
470 let temp_dir = std::env::temp_dir();
471 let safe_id = sanitize_filename(&context.id);
472 let temp_file_path = temp_dir.join(format!("winx-{safe_id}.txt"));
473
474 let mut file = File::create(&temp_file_path).map_err(|e| WinxError::FileAccessError {
475 path: temp_file_path.clone(),
476 message: format!("Failed to create temporary file: {e}"),
477 })?;
478
479 file.write_all(memory_data.as_bytes()).map_err(|e| WinxError::FileAccessError {
480 path: temp_file_path.clone(),
481 message: format!("Failed to write to temporary file: {e}"),
482 })?;
483
484 let path_str = temp_file_path.to_string_lossy().to_string();
485
486 Ok(format!(
487 "Context was saved to temporary file at {path_str} due to permission issues with regular locations."
488 ))
489}
490
491fn sanitize_filename(input: &str) -> String {
493 let invalid_chars = vec!['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
494 let mut result = input.to_string();
495
496 for c in invalid_chars {
497 result = result.replace(c, "_");
498 }
499
500 if result.len() > 50 {
502 use rand::RngExt;
503 result = format!("{}-{}", &result[0..45], rand::rng().random_range(1000..9999));
504 }
505
506 result
507}