git_worktree_manager/operations/
ai_tools.rs1use std::path::Path;
5
6use console::style;
7
8use crate::config::{self, get_ai_tool_command, get_ai_tool_resume_command, is_claude_tool};
9use crate::constants::{LaunchMethod, MAX_SESSION_NAME_LENGTH};
10use crate::error::Result;
11use crate::git;
12use crate::messages;
13use crate::session;
14
15use super::helpers::{resolve_target_strict, resolve_worktree_target};
16use super::launchers;
17use super::spawn_spec::{self, SpawnSpec};
18
19fn dispatch_launch(
25 path: &Path,
26 method: LaunchMethod,
27 session_name: Option<String>,
28 cmd: &str,
29 ai_tool_name: &str,
30) -> Result<()> {
31 match method {
32 LaunchMethod::Foreground => {
33 println!(
34 "{}\n",
35 style(messages::starting_ai_tool_foreground(ai_tool_name)).cyan()
36 );
37 let _session_lock = match crate::operations::lockfile::acquire(path, ai_tool_name) {
40 Ok(lock) => Some(lock),
41 Err(err @ crate::operations::lockfile::AcquireError::ForeignLock(_)) => {
42 return Err(crate::error::CwError::Other(format!(
43 "{}; exit that session first",
44 err
45 )));
46 }
47 Err(e) => {
48 eprintln!(
49 "{} could not write session lock: {}",
50 style("warning:").yellow(),
51 e
52 );
53 None
54 }
55 };
56 launchers::foreground::run(path, cmd);
57 }
58 LaunchMethod::Detach => {
59 launchers::detached::run(path, cmd);
60 println!(
61 "{} {} detached (survives terminal close)\n",
62 style("*").green().bold(),
63 ai_tool_name
64 );
65 }
66 LaunchMethod::ItermWindow => launchers::iterm::launch_window(path, cmd, ai_tool_name)?,
68 LaunchMethod::ItermTab => launchers::iterm::launch_tab(path, cmd, ai_tool_name)?,
69 LaunchMethod::ItermPaneH => launchers::iterm::launch_pane(path, cmd, ai_tool_name, true)?,
70 LaunchMethod::ItermPaneV => launchers::iterm::launch_pane(path, cmd, ai_tool_name, false)?,
71 LaunchMethod::Tmux => {
73 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
74 launchers::tmux::launch_session(path, cmd, ai_tool_name, &sn)?;
75 }
76 LaunchMethod::TmuxWindow => launchers::tmux::launch_window(path, cmd, ai_tool_name)?,
77 LaunchMethod::TmuxPaneH => launchers::tmux::launch_pane(path, cmd, ai_tool_name, true)?,
78 LaunchMethod::TmuxPaneV => launchers::tmux::launch_pane(path, cmd, ai_tool_name, false)?,
79 LaunchMethod::Zellij => {
81 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
82 launchers::zellij::launch_session(path, cmd, ai_tool_name, &sn)?;
83 }
84 LaunchMethod::ZellijTab => launchers::zellij::launch_tab(path, cmd, ai_tool_name)?,
85 LaunchMethod::ZellijPaneH => launchers::zellij::launch_pane(path, cmd, ai_tool_name, true)?,
86 LaunchMethod::ZellijPaneV => {
87 launchers::zellij::launch_pane(path, cmd, ai_tool_name, false)?
88 }
89 LaunchMethod::WeztermWindow => launchers::wezterm::launch_window(path, cmd, ai_tool_name)?,
91 LaunchMethod::WeztermTab => launchers::wezterm::launch_tab(path, cmd, ai_tool_name)?,
92 LaunchMethod::WeztermTabBg => launchers::wezterm::launch_tab_bg(path, cmd, ai_tool_name)?,
93 LaunchMethod::WeztermPaneH => {
94 launchers::wezterm::launch_pane(path, cmd, ai_tool_name, true)?
95 }
96 LaunchMethod::WeztermPaneV => {
97 launchers::wezterm::launch_pane(path, cmd, ai_tool_name, false)?
98 }
99 }
100
101 Ok(())
102}
103
104pub fn launch_ai_tool(path: &Path, resume: bool, term_override: Option<&str>) -> Result<()> {
106 let (method, session_name) = config::resolve_term_option(term_override, path)?;
107
108 let ai_cmd_parts = if resume {
110 get_ai_tool_resume_command()?
111 } else if is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(path) {
112 eprintln!("Found existing Claude session, using --continue");
113 get_ai_tool_resume_command()?
114 } else {
115 get_ai_tool_command()?
116 };
117
118 if ai_cmd_parts.is_empty() {
119 return Ok(());
120 }
121
122 let ai_tool_name = ai_cmd_parts[0].clone();
123
124 if !git::has_command(&ai_tool_name) {
125 println!(
126 "{} {} not detected. Install it or update config with 'gw config set ai-tool <tool>'.\n",
127 style("!").yellow(),
128 ai_tool_name,
129 );
130 return Ok(());
131 }
132
133 let spec = SpawnSpec::new(ai_cmd_parts, path.to_path_buf());
137 let (cmd, _) = spawn_spec::materialize(&spec)?;
140
141 dispatch_launch(path, method, session_name, &cmd, ai_tool_name.as_str())
147}
148
149pub fn resume_worktree(worktree: Option<&str>, term_override: Option<&str>) -> Result<()> {
154 let (worktree_path, branch_name) = if let Some(target) = worktree {
155 let main_repo = git::get_main_repo_root(None)?;
156 let strict = resolve_target_strict(&main_repo, target)?;
157 let branch_name = strict.branch.unwrap_or_else(|| {
158 strict
159 .path
160 .file_name()
161 .map(|n| n.to_string_lossy().into_owned())
162 .unwrap_or_else(|| "(detached)".into())
163 });
164 (strict.path, branch_name)
165 } else {
166 let resolved = resolve_worktree_target(None, None)?;
168 (resolved.path, resolved.branch)
169 };
170
171 if worktree.is_some() {
173 let _ = std::env::set_current_dir(&worktree_path);
174 println!(
175 "{}\n",
176 style(messages::switched_to_worktree(&worktree_path)).dim()
177 );
178 }
179
180 let has_session =
182 is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(&worktree_path);
183
184 if has_session {
185 println!(
186 "{} Found session for branch: {}",
187 style("*").green(),
188 style(&branch_name).bold()
189 );
190
191 if let Some(metadata) = session::load_session_metadata(&branch_name) {
192 println!(" AI tool: {}", style(&metadata.ai_tool).dim());
193 println!(" Last updated: {}", style(&metadata.updated_at).dim());
194 }
195
196 if let Some(context) = session::load_context(&branch_name) {
197 println!("\n{}", style("Previous context:").cyan());
198 println!("{}", style(&context).dim());
199 }
200 println!();
201 } else {
202 println!(
203 "{} No previous session found for branch: {}",
204 style("i").yellow(),
205 style(&branch_name).bold()
206 );
207 println!("{}\n", style("Starting fresh session...").dim());
208 }
209
210 let ai_cmd = if has_session {
212 get_ai_tool_resume_command()?
213 } else {
214 get_ai_tool_command()?
215 };
216
217 if !ai_cmd.is_empty() {
218 let ai_tool_name = &ai_cmd[0];
219 let _ = session::save_session_metadata(
220 &branch_name,
221 ai_tool_name,
222 &worktree_path.to_string_lossy(),
223 );
224
225 if has_session {
226 println!(
227 "{} {}\n",
228 style(messages::resuming_ai_tool_in(ai_tool_name)).cyan(),
229 worktree_path.display()
230 );
231 } else {
232 println!(
233 "{} {}\n",
234 style(messages::starting_ai_tool_in(ai_tool_name)).cyan(),
235 worktree_path.display()
236 );
237 }
238
239 launch_ai_tool(&worktree_path, has_session, term_override)?;
240 }
241
242 Ok(())
243}
244
245pub fn spawn_in_worktree(
250 worktree_path: &Path,
251 prompt: Option<&str>,
252 term_override: Option<&str>,
253) -> Result<()> {
254 let (method, session_name) = config::resolve_term_option(term_override, worktree_path)?;
255
256 let ai_cmd_parts = if let Some(p) = prompt {
257 config::get_ai_tool_merge_command(p)?
258 } else {
259 get_ai_tool_command()?
260 };
261
262 if ai_cmd_parts.is_empty() {
263 return Ok(());
264 }
265
266 let ai_tool_name = ai_cmd_parts[0].clone();
267
268 if !git::has_command(&ai_tool_name) {
269 println!(
270 "{} {} not detected. Install it or update config with 'gw config set ai-tool <tool>'.\n",
271 style("!").yellow(),
272 ai_tool_name,
273 );
274 return Ok(());
275 }
276
277 let spec = SpawnSpec::new(ai_cmd_parts, worktree_path.to_path_buf());
278 let (cmd, _) = spawn_spec::materialize(&spec)?;
279
280 dispatch_launch(
281 worktree_path,
282 method,
283 session_name,
284 &cmd,
285 ai_tool_name.as_str(),
286 )
287}
288
289fn generate_session_name(path: &Path) -> String {
291 let config = config::load_config().unwrap_or_default();
292 let prefix = &config.launch.tmux_session_prefix;
293 let dir_name = path
294 .file_name()
295 .map(|n| n.to_string_lossy().to_string())
296 .unwrap_or_else(|| "worktree".to_string());
297
298 let name = format!("{}-{}", prefix, dir_name);
299 if name.len() > MAX_SESSION_NAME_LENGTH {
300 name[..MAX_SESSION_NAME_LENGTH].to_string()
301 } else {
302 name
303 }
304}