1use 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 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}