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