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