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 {
70 LaunchMethod::Foreground => {
71 println!(
72 "{}\n",
73 style(messages::starting_ai_tool_foreground(ai_tool_name)).cyan()
74 );
75 launchers::foreground::run(path, &cmd);
76 }
77 LaunchMethod::Detach => {
78 launchers::detached::run(path, &cmd);
79 println!(
80 "{} {} detached (survives terminal close)\n",
81 style("*").green().bold(),
82 ai_tool_name
83 );
84 }
85 LaunchMethod::ItermWindow => launchers::iterm::launch_window(path, &cmd, ai_tool_name)?,
87 LaunchMethod::ItermTab => launchers::iterm::launch_tab(path, &cmd, ai_tool_name)?,
88 LaunchMethod::ItermPaneH => launchers::iterm::launch_pane(path, &cmd, ai_tool_name, true)?,
89 LaunchMethod::ItermPaneV => launchers::iterm::launch_pane(path, &cmd, ai_tool_name, false)?,
90 LaunchMethod::Tmux => {
92 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
93 launchers::tmux::launch_session(path, &cmd, ai_tool_name, &sn)?;
94 }
95 LaunchMethod::TmuxWindow => launchers::tmux::launch_window(path, &cmd, ai_tool_name)?,
96 LaunchMethod::TmuxPaneH => launchers::tmux::launch_pane(path, &cmd, ai_tool_name, true)?,
97 LaunchMethod::TmuxPaneV => launchers::tmux::launch_pane(path, &cmd, ai_tool_name, false)?,
98 LaunchMethod::Zellij => {
100 let sn = session_name.unwrap_or_else(|| generate_session_name(path));
101 launchers::zellij::launch_session(path, &cmd, ai_tool_name, &sn)?;
102 }
103 LaunchMethod::ZellijTab => launchers::zellij::launch_tab(path, &cmd, ai_tool_name)?,
104 LaunchMethod::ZellijPaneH => {
105 launchers::zellij::launch_pane(path, &cmd, ai_tool_name, true)?
106 }
107 LaunchMethod::ZellijPaneV => {
108 launchers::zellij::launch_pane(path, &cmd, ai_tool_name, false)?
109 }
110 LaunchMethod::WeztermWindow => launchers::wezterm::launch_window(path, &cmd, ai_tool_name)?,
112 LaunchMethod::WeztermTab => launchers::wezterm::launch_tab(path, &cmd, ai_tool_name)?,
113 LaunchMethod::WeztermTabBg => launchers::wezterm::launch_tab_bg(path, &cmd, ai_tool_name)?,
114 LaunchMethod::WeztermPaneH => {
115 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, true)?
116 }
117 LaunchMethod::WeztermPaneV => {
118 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, false)?
119 }
120 }
121
122 Ok(())
123}
124
125pub fn resume_worktree(
127 worktree: Option<&str>,
128 term: Option<&str>,
129 lookup_mode: Option<&str>,
130) -> Result<()> {
131 let resolved = resolve_worktree_target(worktree, lookup_mode)?;
132 let worktree_path = resolved.path;
133 let branch_name = resolved.branch;
134 let worktree_repo = resolved.repo;
135
136 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
138 let base_branch = git::get_config(&base_key, Some(&worktree_repo)).unwrap_or_default();
139
140 let mut hook_ctx = build_hook_context(
141 &branch_name,
142 &base_branch,
143 &worktree_path,
144 &worktree_repo,
145 "resume.pre",
146 "resume",
147 );
148 hooks::run_hooks(
149 "resume.pre",
150 &hook_ctx,
151 Some(&worktree_path),
152 Some(&worktree_repo),
153 )?;
154
155 if worktree.is_some() {
157 let _ = std::env::set_current_dir(&worktree_path);
158 println!(
159 "{}\n",
160 style(messages::switched_to_worktree(&worktree_path)).dim()
161 );
162 }
163
164 let has_session =
166 is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(&worktree_path);
167
168 if has_session {
169 println!(
170 "{} Found session for branch: {}",
171 style("*").green(),
172 style(&branch_name).bold()
173 );
174
175 if let Some(metadata) = session::load_session_metadata(&branch_name) {
176 println!(" AI tool: {}", style(&metadata.ai_tool).dim());
177 println!(" Last updated: {}", style(&metadata.updated_at).dim());
178 }
179
180 if let Some(context) = session::load_context(&branch_name) {
181 println!("\n{}", style("Previous context:").cyan());
182 println!("{}", style(&context).dim());
183 }
184 println!();
185 } else {
186 println!(
187 "{} No previous session found for branch: {}",
188 style("i").yellow(),
189 style(&branch_name).bold()
190 );
191 println!("{}\n", style("Starting fresh session...").dim());
192 }
193
194 let ai_cmd = if has_session {
196 get_ai_tool_resume_command()?
197 } else {
198 get_ai_tool_command()?
199 };
200
201 if !ai_cmd.is_empty() {
202 let ai_tool_name = &ai_cmd[0];
203 let _ = session::save_session_metadata(
204 &branch_name,
205 ai_tool_name,
206 &worktree_path.to_string_lossy(),
207 );
208
209 if has_session {
210 println!(
211 "{} {}\n",
212 style(messages::resuming_ai_tool_in(ai_tool_name)).cyan(),
213 worktree_path.display()
214 );
215 } else {
216 println!(
217 "{} {}\n",
218 style(messages::starting_ai_tool_in(ai_tool_name)).cyan(),
219 worktree_path.display()
220 );
221 }
222
223 launch_ai_tool(&worktree_path, term, has_session, None, None)?;
224 }
225
226 hook_ctx.insert("event".into(), "resume.post".into());
228 let _ = hooks::run_hooks(
229 "resume.post",
230 &hook_ctx,
231 Some(&worktree_path),
232 Some(&worktree_repo),
233 );
234
235 Ok(())
236}
237
238fn generate_session_name(path: &Path) -> String {
240 let config = config::load_config().unwrap_or_default();
241 let prefix = &config.launch.tmux_session_prefix;
242 let dir_name = path
243 .file_name()
244 .map(|n| n.to_string_lossy().to_string())
245 .unwrap_or_else(|| "worktree".to_string());
246
247 let name = format!("{}-{}", prefix, dir_name);
248 if name.len() > MAX_SESSION_NAME_LENGTH {
249 name[..MAX_SESSION_NAME_LENGTH].to_string()
250 } else {
251 name
252 }
253}
254
255fn shell_quote_join(parts: &[String]) -> String {
257 parts
258 .iter()
259 .map(|p| {
260 if p.contains(char::is_whitespace) || p.contains('\'') || p.contains('"') {
261 format!("'{}'", p.replace('\'', "'\\''"))
262 } else {
263 p.clone()
264 }
265 })
266 .collect::<Vec<_>>()
267 .join(" ")
268}