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