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