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