git_worktree_manager/operations/
ai_tools.rs1use std::collections::HashMap;
6use std::path::Path;
7
8use console::style;
9
10use crate::config::{
11 self, get_ai_tool_command, get_ai_tool_resume_command, is_claude_tool, parse_term_option,
12};
13use crate::constants::{
14 format_config_key, LaunchMethod, CONFIG_KEY_BASE_BRANCH, MAX_SESSION_NAME_LENGTH,
15};
16use crate::error::Result;
17use crate::git;
18use crate::hooks;
19use crate::session;
20
21use super::helpers::resolve_worktree_target;
22use super::launchers;
23
24pub fn launch_ai_tool(
26 path: &Path,
27 term: Option<&str>,
28 resume: bool,
29 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 resume {
37 get_ai_tool_resume_command()?
38 } else {
39 if is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(path) {
41 eprintln!("Found existing Claude session, using --continue");
42 get_ai_tool_resume_command()?
43 } else {
44 get_ai_tool_command()?
45 }
46 };
47
48 if ai_cmd_parts.is_empty() {
49 return Ok(());
50 }
51
52 let ai_tool_name = &ai_cmd_parts[0];
53
54 if !git::has_command(ai_tool_name) {
55 println!(
56 "{} {} not detected. Install it or update config with 'cw config set ai-tool <tool>'.\n",
57 style("!").yellow(),
58 ai_tool_name,
59 );
60 return Ok(());
61 }
62
63 let cmd = shell_quote_join(&ai_cmd_parts);
65
66 match method {
68 LaunchMethod::Foreground => {
69 println!(
70 "{}\n",
71 style(format!("Starting {} (Ctrl+C to exit)...", ai_tool_name)).cyan()
72 );
73 launchers::foreground::run(path, &cmd);
74 }
75 LaunchMethod::Detach => {
76 launchers::detached::run(path, &cmd);
77 println!(
78 "{} {} detached (survives terminal close)\n",
79 style("*").green().bold(),
80 ai_tool_name
81 );
82 }
83 LaunchMethod::ItermWindow => launchers::iterm::launch_window(path, &cmd, ai_tool_name)?,
85 LaunchMethod::ItermTab => launchers::iterm::launch_tab(path, &cmd, ai_tool_name)?,
86 LaunchMethod::ItermPaneH => launchers::iterm::launch_pane(path, &cmd, ai_tool_name, true)?,
87 LaunchMethod::ItermPaneV => launchers::iterm::launch_pane(path, &cmd, ai_tool_name, false)?,
88 LaunchMethod::Tmux => {
90 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
91 launchers::tmux::launch_session(path, &cmd, ai_tool_name, &sn)?;
92 }
93 LaunchMethod::TmuxWindow => launchers::tmux::launch_window(path, &cmd, ai_tool_name)?,
94 LaunchMethod::TmuxPaneH => launchers::tmux::launch_pane(path, &cmd, ai_tool_name, true)?,
95 LaunchMethod::TmuxPaneV => launchers::tmux::launch_pane(path, &cmd, ai_tool_name, false)?,
96 LaunchMethod::Zellij => {
98 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
99 launchers::zellij::launch_session(path, &cmd, ai_tool_name, &sn)?;
100 }
101 LaunchMethod::ZellijTab => launchers::zellij::launch_tab(path, &cmd, ai_tool_name)?,
102 LaunchMethod::ZellijPaneH => {
103 launchers::zellij::launch_pane(path, &cmd, ai_tool_name, true)?
104 }
105 LaunchMethod::ZellijPaneV => {
106 launchers::zellij::launch_pane(path, &cmd, ai_tool_name, false)?
107 }
108 LaunchMethod::WeztermWindow => launchers::wezterm::launch_window(path, &cmd, ai_tool_name)?,
110 LaunchMethod::WeztermTab => launchers::wezterm::launch_tab(path, &cmd, ai_tool_name)?,
111 LaunchMethod::WeztermPaneH => {
112 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, true)?
113 }
114 LaunchMethod::WeztermPaneV => {
115 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, false)?
116 }
117 }
118
119 Ok(())
120}
121
122pub fn resume_worktree(worktree: Option<&str>, term: Option<&str>) -> Result<()> {
124 let (worktree_path, branch_name, worktree_repo) = resolve_worktree_target(worktree, None)?;
125
126 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
128 let base_branch = git::get_config(&base_key, Some(&worktree_repo)).unwrap_or_default();
129
130 let mut hook_ctx = HashMap::new();
131 hook_ctx.insert("branch".into(), branch_name.clone());
132 hook_ctx.insert("base_branch".into(), base_branch);
133 hook_ctx.insert(
134 "worktree_path".into(),
135 worktree_path.to_string_lossy().to_string(),
136 );
137 hook_ctx.insert(
138 "repo_path".into(),
139 worktree_repo.to_string_lossy().to_string(),
140 );
141 hook_ctx.insert("event".into(), "resume.pre".into());
142 hook_ctx.insert("operation".into(), "resume".into());
143 hooks::run_hooks(
144 "resume.pre",
145 &hook_ctx,
146 Some(&worktree_path),
147 Some(&worktree_repo),
148 )?;
149
150 if worktree.is_some() {
152 let _ = std::env::set_current_dir(&worktree_path);
153 println!(
154 "{}\n",
155 style(format!("Switched to worktree: {}", worktree_path.display())).dim()
156 );
157 }
158
159 let has_session =
161 is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(&worktree_path);
162
163 if has_session {
164 println!(
165 "{} Found session for branch: {}",
166 style("*").green(),
167 style(&branch_name).bold()
168 );
169
170 if let Some(metadata) = session::load_session_metadata(&branch_name) {
171 println!(" AI tool: {}", style(&metadata.ai_tool).dim());
172 println!(" Last updated: {}", style(&metadata.updated_at).dim());
173 }
174
175 if let Some(context) = session::load_context(&branch_name) {
176 println!("\n{}", style("Previous context:").cyan());
177 println!("{}", style(&context).dim());
178 }
179 println!();
180 } else {
181 println!(
182 "{} No previous session found for branch: {}",
183 style("i").yellow(),
184 style(&branch_name).bold()
185 );
186 println!("{}\n", style("Starting fresh session...").dim());
187 }
188
189 let ai_cmd = if has_session {
191 get_ai_tool_resume_command()?
192 } else {
193 get_ai_tool_command()?
194 };
195
196 if !ai_cmd.is_empty() {
197 let ai_tool_name = &ai_cmd[0];
198 let _ = session::save_session_metadata(
199 &branch_name,
200 ai_tool_name,
201 &worktree_path.to_string_lossy(),
202 );
203
204 if has_session {
205 println!(
206 "{} {}\n",
207 style(format!("Resuming {} in:", ai_tool_name)).cyan(),
208 worktree_path.display()
209 );
210 } else {
211 println!(
212 "{} {}\n",
213 style(format!("Starting {} in:", ai_tool_name)).cyan(),
214 worktree_path.display()
215 );
216 }
217
218 launch_ai_tool(&worktree_path, term, has_session, None)?;
219 }
220
221 hook_ctx.insert("event".into(), "resume.post".into());
223 let _ = hooks::run_hooks(
224 "resume.post",
225 &hook_ctx,
226 Some(&worktree_path),
227 Some(&worktree_repo),
228 );
229
230 Ok(())
231}
232
233fn generate_session_name(path: &Path) -> String {
235 let config = config::load_config().unwrap_or_default();
236 let prefix = &config.launch.tmux_session_prefix;
237 let dir_name = path
238 .file_name()
239 .map(|n| n.to_string_lossy().to_string())
240 .unwrap_or_else(|| "worktree".to_string());
241
242 let name = format!("{}-{}", prefix, dir_name);
243 if name.len() > MAX_SESSION_NAME_LENGTH {
244 name[..MAX_SESSION_NAME_LENGTH].to_string()
245 } else {
246 name
247 }
248}
249
250fn shell_quote_join(parts: &[String]) -> String {
252 parts
253 .iter()
254 .map(|p| {
255 if p.contains(char::is_whitespace) || p.contains('\'') || p.contains('"') {
256 format!("'{}'", p.replace('\'', "'\\''"))
257 } else {
258 p.clone()
259 }
260 })
261 .collect::<Vec<_>>()
262 .join(" ")
263}