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