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::WeztermPaneH => {
114 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, true)?
115 }
116 LaunchMethod::WeztermPaneV => {
117 launchers::wezterm::launch_pane(path, &cmd, ai_tool_name, false)?
118 }
119 }
120
121 Ok(())
122}
123
124pub fn resume_worktree(
126 worktree: Option<&str>,
127 term: Option<&str>,
128 lookup_mode: Option<&str>,
129) -> Result<()> {
130 let resolved = resolve_worktree_target(worktree, lookup_mode)?;
131 let worktree_path = resolved.path;
132 let branch_name = resolved.branch;
133 let worktree_repo = resolved.repo;
134
135 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
137 let base_branch = git::get_config(&base_key, Some(&worktree_repo)).unwrap_or_default();
138
139 let mut hook_ctx = build_hook_context(
140 &branch_name,
141 &base_branch,
142 &worktree_path,
143 &worktree_repo,
144 "resume.pre",
145 "resume",
146 );
147 hooks::run_hooks(
148 "resume.pre",
149 &hook_ctx,
150 Some(&worktree_path),
151 Some(&worktree_repo),
152 )?;
153
154 if worktree.is_some() {
156 let _ = std::env::set_current_dir(&worktree_path);
157 println!(
158 "{}\n",
159 style(messages::switched_to_worktree(&worktree_path)).dim()
160 );
161 }
162
163 let has_session =
165 is_claude_tool().unwrap_or(false) && session::claude_native_session_exists(&worktree_path);
166
167 if has_session {
168 println!(
169 "{} Found session for branch: {}",
170 style("*").green(),
171 style(&branch_name).bold()
172 );
173
174 if let Some(metadata) = session::load_session_metadata(&branch_name) {
175 println!(" AI tool: {}", style(&metadata.ai_tool).dim());
176 println!(" Last updated: {}", style(&metadata.updated_at).dim());
177 }
178
179 if let Some(context) = session::load_context(&branch_name) {
180 println!("\n{}", style("Previous context:").cyan());
181 println!("{}", style(&context).dim());
182 }
183 println!();
184 } else {
185 println!(
186 "{} No previous session found for branch: {}",
187 style("i").yellow(),
188 style(&branch_name).bold()
189 );
190 println!("{}\n", style("Starting fresh session...").dim());
191 }
192
193 let ai_cmd = if has_session {
195 get_ai_tool_resume_command()?
196 } else {
197 get_ai_tool_command()?
198 };
199
200 if !ai_cmd.is_empty() {
201 let ai_tool_name = &ai_cmd[0];
202 let _ = session::save_session_metadata(
203 &branch_name,
204 ai_tool_name,
205 &worktree_path.to_string_lossy(),
206 );
207
208 if has_session {
209 println!(
210 "{} {}\n",
211 style(messages::resuming_ai_tool_in(ai_tool_name)).cyan(),
212 worktree_path.display()
213 );
214 } else {
215 println!(
216 "{} {}\n",
217 style(messages::starting_ai_tool_in(ai_tool_name)).cyan(),
218 worktree_path.display()
219 );
220 }
221
222 launch_ai_tool(&worktree_path, term, has_session, None, None)?;
223 }
224
225 hook_ctx.insert("event".into(), "resume.post".into());
227 let _ = hooks::run_hooks(
228 "resume.post",
229 &hook_ctx,
230 Some(&worktree_path),
231 Some(&worktree_repo),
232 );
233
234 Ok(())
235}
236
237fn generate_session_name(path: &Path) -> String {
239 let config = config::load_config().unwrap_or_default();
240 let prefix = &config.launch.tmux_session_prefix;
241 let dir_name = path
242 .file_name()
243 .map(|n| n.to_string_lossy().to_string())
244 .unwrap_or_else(|| "worktree".to_string());
245
246 let name = format!("{}-{}", prefix, dir_name);
247 if name.len() > MAX_SESSION_NAME_LENGTH {
248 name[..MAX_SESSION_NAME_LENGTH].to_string()
249 } else {
250 name
251 }
252}
253
254fn shell_quote_join(parts: &[String]) -> String {
256 parts
257 .iter()
258 .map(|p| {
259 if p.contains(char::is_whitespace) || p.contains('\'') || p.contains('"') {
260 format!("'{}'", p.replace('\'', "'\\''"))
261 } else {
262 p.clone()
263 }
264 })
265 .collect::<Vec<_>>()
266 .join(" ")
267}