Skip to main content

winx_code_agent/tools/
initialize.rs

1//! Implementation of the Initialize tool.
2
3use std::fmt::Write as FmtWrite;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use tokio::sync::Mutex;
8use tracing::{debug, info, instrument, warn};
9
10use crate::errors::{Result, WinxError};
11use crate::state::bash_state::{generate_thread_id, BashState};
12use crate::types::{
13    normalize_thread_id, AllowedCommands, AllowedGlobs, BashCommandMode, BashMode,
14    CodeWriterConfig, FileEditMode, Initialize, InitializeType, ModeName, Modes, WriteIfEmptyMode,
15};
16use crate::utils::mmap::read_file_to_string;
17use crate::utils::path::{ensure_directory_exists, expand_user, validate_path_in_workspace};
18
19#[inline]
20fn convert_mode_name(mode_name: &ModeName) -> Modes {
21    match mode_name {
22        ModeName::Wcgw => Modes::Wcgw,
23        ModeName::Architect => Modes::Architect,
24        ModeName::CodeWriter => Modes::CodeWriter,
25    }
26}
27
28fn code_writer_state(
29    config: &CodeWriterConfig,
30    workspace_root: &Path,
31) -> (BashCommandMode, FileEditMode, WriteIfEmptyMode) {
32    let mut config = config.clone();
33    config.update_relative_globs(&workspace_root.to_string_lossy());
34
35    (
36        BashCommandMode {
37            bash_mode: BashMode::NormalMode,
38            allowed_commands: config.allowed_commands,
39        },
40        FileEditMode { allowed_globs: config.allowed_globs.clone() },
41        WriteIfEmptyMode { allowed_globs: config.allowed_globs },
42    )
43}
44
45fn mode_to_state(
46    mode: Modes,
47    config: Option<&CodeWriterConfig>,
48    workspace_root: &Path,
49) -> Result<(BashCommandMode, FileEditMode, WriteIfEmptyMode)> {
50    match mode {
51        Modes::Wcgw => Ok((
52            BashCommandMode {
53                bash_mode: BashMode::NormalMode,
54                allowed_commands: AllowedCommands::All("all".to_string()),
55            },
56            FileEditMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
57            WriteIfEmptyMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
58        )),
59        Modes::Architect => Ok((
60            BashCommandMode {
61                bash_mode: BashMode::RestrictedMode,
62                allowed_commands: AllowedCommands::All("all".to_string()),
63            },
64            FileEditMode { allowed_globs: AllowedGlobs::List(vec![]) },
65            WriteIfEmptyMode { allowed_globs: AllowedGlobs::List(vec![]) },
66        )),
67        Modes::CodeWriter => {
68            let config = config.ok_or_else(|| {
69                WinxError::ArgumentParseError(
70                    "code_writer_config is required when mode_name is code_writer.".to_string(),
71                )
72            })?;
73            Ok(code_writer_state(config, workspace_root))
74        }
75    }
76}
77
78fn read_initial_files_simple(files: &[String], workspace: &std::path::Path) -> String {
79    let mut output = String::new();
80    for file_path in files {
81        let expanded = expand_user(file_path);
82        let path = if std::path::Path::new(&expanded).is_absolute() {
83            PathBuf::from(&expanded)
84        } else {
85            workspace.join(&expanded)
86        };
87
88        if let Ok(validated) = validate_path_in_workspace(&path, workspace) {
89            if validated.exists() && validated.is_file() {
90                if let Ok(content) = read_file_to_string(&validated, 10_000_000) {
91                    let _ = write!(output, "\n{file_path}\n```\n{content}\n```\n");
92                }
93            }
94        }
95    }
96    output
97}
98
99fn prepare_workspace(initialize: &Initialize, response: &mut String) -> Result<PathBuf> {
100    let workspace_path_str = expand_user(&initialize.any_workspace_path);
101    if workspace_path_str.is_empty() {
102        return Err(WinxError::WorkspacePathError("Workspace path cannot be empty.".to_string()));
103    }
104
105    let workspace_path = PathBuf::from(&workspace_path_str);
106    let mut folder_to_start = workspace_path.clone();
107
108    if workspace_path.exists() {
109        if workspace_path.is_file() {
110            folder_to_start = workspace_path.parent().unwrap_or(&workspace_path).to_path_buf();
111            let _ =
112                writeln!(response, "Using parent directory of file: {}", folder_to_start.display());
113        } else if workspace_path.is_dir() {
114            let _ = writeln!(response, "Using workspace directory: {}", folder_to_start.display());
115        }
116    } else if workspace_path.is_absolute() {
117        ensure_directory_exists(&workspace_path).map_err(|e| {
118            WinxError::WorkspacePathError(format!("Failed to create workspace: {e}"))
119        })?;
120        let _ = writeln!(response, "Created workspace directory: {}", workspace_path.display());
121    }
122
123    // Canonicalize so downstream comparisons (workspace checks, glob prefixes) match
124    // paths that were canonicalized via fs::canonicalize — important on macOS where
125    // /var, /tmp etc. are symlinks to /private/var, /private/tmp.
126    if folder_to_start.exists() {
127        if let Ok(canonical) = folder_to_start.canonicalize() {
128            folder_to_start = canonical;
129        }
130    }
131
132    Ok(folder_to_start)
133}
134
135fn initialize_thread_id(initialize: &Initialize) -> String {
136    let thread_id = normalize_thread_id(&initialize.thread_id);
137    if thread_id.is_empty() {
138        generate_thread_id()
139    } else {
140        thread_id
141    }
142}
143
144fn validate_thread_id(initialize: &Initialize) -> Result<()> {
145    if initialize.init_type != InitializeType::FirstCall
146        && normalize_thread_id(&initialize.thread_id).is_empty()
147    {
148        return Err(WinxError::ThreadIdMismatch(
149            "Thread id should be provided if type != 'first_call', including when resetting."
150                .to_string(),
151        ));
152    }
153
154    Ok(())
155}
156
157fn load_guidelines(workspace: &Path) -> String {
158    let mut output = String::new();
159    let mut candidates = Vec::new();
160    if let Some(home) = home::home_dir() {
161        candidates.push(home.join(".winx").join("AGENTS.md"));
162        candidates.push(home.join(".winx").join("CLAUDE.md"));
163        candidates.push(home.join(".wcgw").join("AGENTS.md"));
164        candidates.push(home.join(".wcgw").join("CLAUDE.md"));
165    }
166    candidates.push(workspace.join("AGENTS.md"));
167    candidates.push(workspace.join("CLAUDE.md"));
168
169    for path in candidates {
170        if path.is_file() {
171            if let Ok(content) = fs::read_to_string(&path) {
172                let _ = writeln!(output, "\n## {}\n{}", path.display(), content);
173            }
174        }
175    }
176    output
177}
178
179#[instrument(level = "info", skip(bash_state_arc, initialize))]
180#[allow(clippy::too_many_lines)]
181pub async fn handle_tool_call(
182    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
183    initialize: Initialize,
184) -> Result<String> {
185    let mut response = String::new();
186
187    info!("Initialize called for workspace: {}", initialize.any_workspace_path);
188
189    validate_thread_id(&initialize)?;
190    let folder_to_start = prepare_workspace(&initialize, &mut response)?;
191    let thread_id = initialize_thread_id(&initialize);
192
193    let mut bash_state_guard = bash_state_arc.lock().await;
194    let mode = convert_mode_name(&initialize.mode_name);
195    let (bash_command_mode, file_edit_mode, write_if_empty_mode) =
196        mode_to_state(mode, initialize.code_writer_config.as_ref(), &folder_to_start)?;
197
198    match initialize.init_type {
199        InitializeType::FirstCall => {
200            let mut new_bash_state = BashState::new();
201            new_bash_state.current_thread_id.clone_from(&thread_id);
202            new_bash_state.mode = mode;
203            new_bash_state.bash_command_mode = bash_command_mode;
204            new_bash_state.file_edit_mode = file_edit_mode;
205            new_bash_state.write_if_empty_mode = write_if_empty_mode;
206            new_bash_state.initialized = true;
207
208            let resumed_context = if initialize.task_id_to_resume.is_empty() {
209                None
210            } else {
211                crate::tools::context_save::load_saved_context(&initialize.task_id_to_resume)?
212            };
213
214            if let Some((memory_data, snapshot)) = &resumed_context {
215                if let Some(snapshot) = snapshot {
216                    new_bash_state.apply_snapshot(snapshot);
217                    new_bash_state.current_thread_id.clone_from(&thread_id);
218                }
219                let _ = writeln!(
220                    response,
221                    "\n# Resumed task {}\nFollowing is the retrieved task context:\n{}",
222                    initialize.task_id_to_resume, memory_data
223                );
224            }
225
226            if resumed_context.as_ref().and_then(|(_, snapshot)| snapshot.as_ref()).is_none()
227                && folder_to_start.exists()
228            {
229                new_bash_state.update_cwd(&folder_to_start)?;
230                new_bash_state.update_workspace_root(&folder_to_start)?;
231            }
232            if new_bash_state.cwd.exists() {
233                new_bash_state.init_pty_shell().await?;
234            }
235
236            let attach_hint = {
237                let pty_guard = new_bash_state.pty_shell.lock().await;
238                pty_guard.as_ref().and_then(|shell| shell.attach_hint.clone())
239            };
240
241            *bash_state_guard = Some(new_bash_state);
242
243            let _ = write!(
244                response,
245                "\n# Environment\nSystem: {}\nMachine: {}\nInitialized in directory: {}\n",
246                std::env::consts::OS,
247                std::env::consts::ARCH,
248                bash_state_guard
249                    .as_ref()
250                    .map_or(folder_to_start.as_path(), |state| state.cwd.as_path())
251                    .display()
252            );
253
254            let _ = writeln!(response, "\nUse thread_id={thread_id} for all winx tool calls.");
255            if let Some(attach_hint) = attach_hint {
256                let _ = writeln!(response, "\nAttach terminal: {attach_hint}");
257            }
258
259            let active_workspace = bash_state_guard
260                .as_ref()
261                .map_or(folder_to_start.as_path(), |state| state.workspace_root.as_path());
262
263            let guidelines = load_guidelines(active_workspace);
264            if !guidelines.is_empty() {
265                let _ = writeln!(response, "\n# Agent guidelines\n{guidelines}");
266            }
267
268            if let Ok((repo_context, _)) = crate::utils::repo::get_repo_context(active_workspace) {
269                let _ = writeln!(response, "\n# Workspace structure\n{repo_context}");
270            }
271
272            if !initialize.initial_files_to_read.is_empty() {
273                let content =
274                    read_initial_files_simple(&initialize.initial_files_to_read, active_workspace);
275                if !content.is_empty() {
276                    let _ = writeln!(response, "\n# Requested files\n{content}");
277                }
278            }
279        }
280        InitializeType::UserAskedModeChange => {
281            if let Some(state) = bash_state_guard.as_mut() {
282                state.mode = mode;
283                state.bash_command_mode = bash_command_mode;
284                state.file_edit_mode = file_edit_mode;
285                state.write_if_empty_mode = write_if_empty_mode;
286                let _ = writeln!(response, "Changed mode to: {mode:?}");
287            } else {
288                return Err(WinxError::BashStateNotInitialized);
289            }
290        }
291        InitializeType::ResetShell => {
292            if let Some(state) = bash_state_guard.as_mut() {
293                state.mode = mode;
294                state.bash_command_mode = bash_command_mode;
295                state.file_edit_mode = file_edit_mode;
296                state.write_if_empty_mode = write_if_empty_mode;
297                state.init_pty_shell().await?;
298                response.push_str("Reset shell (new PTY created)\n");
299            } else {
300                return Err(WinxError::BashStateNotInitialized);
301            }
302        }
303        InitializeType::UserAskedChangeWorkspace => {
304            if let Some(state) = bash_state_guard.as_mut() {
305                if folder_to_start.exists() {
306                    state.update_cwd(&folder_to_start)?;
307                    state.update_workspace_root(&folder_to_start)?;
308                    let _ =
309                        writeln!(response, "Changed workspace to: {}", folder_to_start.display());
310                } else {
311                    let _ = writeln!(
312                        response,
313                        "Warning: Workspace path {} does not exist",
314                        folder_to_start.display()
315                    );
316                }
317            } else {
318                return Err(WinxError::BashStateNotInitialized);
319            }
320        }
321    }
322
323    Ok(response)
324}