omni_dev/cli/
git.rs

1//! Git-related CLI commands
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use tracing::{debug, error};
6
7/// Git operations
8#[derive(Parser)]
9pub struct GitCommand {
10    /// Git subcommand to execute
11    #[command(subcommand)]
12    pub command: GitSubcommands,
13}
14
15/// Git subcommands
16#[derive(Subcommand)]
17pub enum GitSubcommands {
18    /// Commit-related operations
19    Commit(CommitCommand),
20    /// Branch-related operations
21    Branch(BranchCommand),
22}
23
24/// Commit operations
25#[derive(Parser)]
26pub struct CommitCommand {
27    /// Commit subcommand to execute
28    #[command(subcommand)]
29    pub command: CommitSubcommands,
30}
31
32/// Commit subcommands
33#[derive(Subcommand)]
34pub enum CommitSubcommands {
35    /// Commit message operations
36    Message(MessageCommand),
37}
38
39/// Message operations
40#[derive(Parser)]
41pub struct MessageCommand {
42    /// Message subcommand to execute
43    #[command(subcommand)]
44    pub command: MessageSubcommands,
45}
46
47/// Message subcommands
48#[derive(Subcommand)]
49pub enum MessageSubcommands {
50    /// Analyze commits and output repository information in YAML format
51    View(ViewCommand),
52    /// Amend commit messages based on a YAML configuration file
53    Amend(AmendCommand),
54    /// AI-powered commit message improvement using Claude
55    Twiddle(TwiddleCommand),
56}
57
58/// View command options
59#[derive(Parser)]
60pub struct ViewCommand {
61    /// Commit range to analyze (e.g., HEAD~3..HEAD, abc123..def456)
62    #[arg(value_name = "COMMIT_RANGE")]
63    pub commit_range: Option<String>,
64}
65
66/// Amend command options  
67#[derive(Parser)]
68pub struct AmendCommand {
69    /// YAML file containing commit amendments
70    #[arg(value_name = "YAML_FILE")]
71    pub yaml_file: String,
72}
73
74/// Twiddle command options
75#[derive(Parser)]
76pub struct TwiddleCommand {
77    /// Commit range to analyze and improve (e.g., HEAD~3..HEAD, abc123..def456)
78    #[arg(value_name = "COMMIT_RANGE")]
79    pub commit_range: Option<String>,
80
81    /// Claude API model to use (if not specified, uses settings or default)
82    #[arg(long)]
83    pub model: Option<String>,
84
85    /// Skip confirmation prompt and apply amendments automatically
86    #[arg(long)]
87    pub auto_apply: bool,
88
89    /// Save generated amendments to file without applying
90    #[arg(long, value_name = "FILE")]
91    pub save_only: Option<String>,
92
93    /// Use additional project context for better suggestions (Phase 3)
94    #[arg(long, default_value = "true")]
95    pub use_context: bool,
96
97    /// Path to custom context directory (defaults to .omni-dev/)
98    #[arg(long)]
99    pub context_dir: Option<std::path::PathBuf>,
100
101    /// Specify work context (e.g., "feature: user authentication")
102    #[arg(long)]
103    pub work_context: Option<String>,
104
105    /// Override detected branch context
106    #[arg(long)]
107    pub branch_context: Option<String>,
108
109    /// Disable contextual analysis (use basic prompting only)
110    #[arg(long)]
111    pub no_context: bool,
112
113    /// Maximum number of commits to process in a single batch (default: 4)
114    #[arg(long, default_value = "4")]
115    pub batch_size: usize,
116
117    /// Skip AI processing and only output repository YAML
118    #[arg(long)]
119    pub no_ai: bool,
120}
121
122/// Branch operations
123#[derive(Parser)]
124pub struct BranchCommand {
125    /// Branch subcommand to execute
126    #[command(subcommand)]
127    pub command: BranchSubcommands,
128}
129
130/// Branch subcommands
131#[derive(Subcommand)]
132pub enum BranchSubcommands {
133    /// Analyze branch commits and output repository information in YAML format
134    Info(InfoCommand),
135    /// Create operations
136    Create(CreateCommand),
137}
138
139/// Info command options
140#[derive(Parser)]
141pub struct InfoCommand {
142    /// Base branch to compare against (defaults to main/master)
143    #[arg(value_name = "BASE_BRANCH")]
144    pub base_branch: Option<String>,
145}
146
147/// Create operations
148#[derive(Parser)]
149pub struct CreateCommand {
150    /// Create subcommand to execute
151    #[command(subcommand)]
152    pub command: CreateSubcommands,
153}
154
155/// Create subcommands
156#[derive(Subcommand)]
157pub enum CreateSubcommands {
158    /// Create a pull request with AI-generated description
159    Pr(CreatePrCommand),
160}
161
162/// Create PR command options
163#[derive(Parser)]
164pub struct CreatePrCommand {
165    /// Base branch to compare against (defaults to main/master)
166    #[arg(value_name = "BASE_BRANCH")]
167    pub base_branch: Option<String>,
168
169    /// Skip confirmation prompt and create PR automatically
170    #[arg(long)]
171    pub auto_apply: bool,
172
173    /// Save generated PR details to file without creating PR
174    #[arg(long, value_name = "FILE")]
175    pub save_only: Option<String>,
176
177    /// Create PR as ready for review (overrides default)
178    #[arg(long, conflicts_with = "draft")]
179    pub ready: bool,
180
181    /// Create PR as draft (overrides default)
182    #[arg(long, conflicts_with = "ready")]
183    pub draft: bool,
184}
185
186impl GitCommand {
187    /// Execute git command
188    pub fn execute(self) -> Result<()> {
189        match self.command {
190            GitSubcommands::Commit(commit_cmd) => commit_cmd.execute(),
191            GitSubcommands::Branch(branch_cmd) => branch_cmd.execute(),
192        }
193    }
194}
195
196impl CommitCommand {
197    /// Execute commit command
198    pub fn execute(self) -> Result<()> {
199        match self.command {
200            CommitSubcommands::Message(message_cmd) => message_cmd.execute(),
201        }
202    }
203}
204
205impl MessageCommand {
206    /// Execute message command
207    pub fn execute(self) -> Result<()> {
208        match self.command {
209            MessageSubcommands::View(view_cmd) => view_cmd.execute(),
210            MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
211            MessageSubcommands::Twiddle(twiddle_cmd) => {
212                // Use tokio runtime for async execution
213                let rt =
214                    tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
215                rt.block_on(twiddle_cmd.execute())
216            }
217        }
218    }
219}
220
221impl ViewCommand {
222    /// Execute view command
223    pub fn execute(self) -> Result<()> {
224        use crate::data::{
225            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
226            WorkingDirectoryInfo,
227        };
228        use crate::git::{GitRepository, RemoteInfo};
229        use crate::utils::ai_scratch;
230
231        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
232
233        // Open git repository
234        let repo = GitRepository::open()
235            .context("Failed to open git repository. Make sure you're in a git repository.")?;
236
237        // Get working directory status
238        let wd_status = repo.get_working_directory_status()?;
239        let working_directory = WorkingDirectoryInfo {
240            clean: wd_status.clean,
241            untracked_changes: wd_status
242                .untracked_changes
243                .into_iter()
244                .map(|fs| FileStatusInfo {
245                    status: fs.status,
246                    file: fs.file,
247                })
248                .collect(),
249        };
250
251        // Get remote information
252        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
253
254        // Parse commit range and get commits
255        let commits = repo.get_commits_in_range(commit_range)?;
256
257        // Create version information
258        let versions = Some(VersionInfo {
259            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
260        });
261
262        // Get AI scratch directory
263        let ai_scratch_path =
264            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
265        let ai_info = AiInfo {
266            scratch: ai_scratch_path.to_string_lossy().to_string(),
267        };
268
269        // Build repository view
270        let mut repo_view = RepositoryView {
271            versions,
272            explanation: FieldExplanation::default(),
273            working_directory,
274            remotes,
275            ai: ai_info,
276            branch_info: None,
277            pr_template: None,
278            pr_template_location: None,
279            branch_prs: None,
280            commits,
281        };
282
283        // Update field presence based on actual data
284        repo_view.update_field_presence();
285
286        // Output as YAML
287        let yaml_output = crate::data::to_yaml(&repo_view)?;
288        println!("{}", yaml_output);
289
290        Ok(())
291    }
292}
293
294impl AmendCommand {
295    /// Execute amend command
296    pub fn execute(self) -> Result<()> {
297        use crate::git::AmendmentHandler;
298
299        println!("๐Ÿ”„ Starting commit amendment process...");
300        println!("๐Ÿ“„ Loading amendments from: {}", self.yaml_file);
301
302        // Create amendment handler and apply amendments
303        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
304
305        handler
306            .apply_amendments(&self.yaml_file)
307            .context("Failed to apply amendments")?;
308
309        Ok(())
310    }
311}
312
313impl TwiddleCommand {
314    /// Execute twiddle command with contextual intelligence
315    pub async fn execute(self) -> Result<()> {
316        // If --no-ai flag is set, skip AI processing and output YAML directly
317        if self.no_ai {
318            return self.execute_no_ai().await;
319        }
320
321        // Determine if contextual analysis should be used
322        let use_contextual = self.use_context && !self.no_context;
323
324        if use_contextual {
325            println!(
326                "๐Ÿช„ Starting AI-powered commit message improvement with contextual intelligence..."
327            );
328        } else {
329            println!("๐Ÿช„ Starting AI-powered commit message improvement...");
330        }
331
332        // 1. Generate repository view to get all commits
333        let full_repo_view = self.generate_repository_view().await?;
334
335        // 2. Check if batching is needed
336        if full_repo_view.commits.len() > self.batch_size {
337            println!(
338                "๐Ÿ“ฆ Processing {} commits in batches of {} to ensure reliable analysis...",
339                full_repo_view.commits.len(),
340                self.batch_size
341            );
342            return self
343                .execute_with_batching(use_contextual, full_repo_view)
344                .await;
345        }
346
347        // 3. Collect contextual information (Phase 3)
348        let context = if use_contextual {
349            Some(self.collect_context(&full_repo_view).await?)
350        } else {
351            None
352        };
353
354        // 4. Show context summary if available
355        if let Some(ref ctx) = context {
356            self.show_context_summary(ctx)?;
357        }
358
359        // 5. Initialize Claude client
360        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
361
362        // Show model information
363        self.show_model_info_from_client(&claude_client)?;
364
365        // 6. Generate amendments via Claude API with context
366        if use_contextual && context.is_some() {
367            println!("๐Ÿค– Analyzing commits with enhanced contextual intelligence...");
368        } else {
369            println!("๐Ÿค– Analyzing commits with Claude AI...");
370        }
371
372        let amendments = if let Some(ctx) = context {
373            claude_client
374                .generate_contextual_amendments(&full_repo_view, &ctx)
375                .await?
376        } else {
377            claude_client.generate_amendments(&full_repo_view).await?
378        };
379
380        // 6. Handle different output modes
381        if let Some(save_path) = self.save_only {
382            amendments.save_to_file(save_path)?;
383            println!("๐Ÿ’พ Amendments saved to file");
384            return Ok(());
385        }
386
387        // 7. Handle amendments
388        if !amendments.amendments.is_empty() {
389            // Create temporary file for amendments
390            let temp_dir = tempfile::tempdir()?;
391            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
392            amendments.save_to_file(&amendments_file)?;
393
394            // Show file path and get user choice
395            if !self.auto_apply && !self.handle_amendments_file(&amendments_file, &amendments)? {
396                println!("โŒ Amendment cancelled by user");
397                return Ok(());
398            }
399
400            // 8. Apply amendments (re-read from file to capture any user edits)
401            self.apply_amendments_from_file(&amendments_file).await?;
402            println!("โœ… Commit messages improved successfully!");
403        } else {
404            println!("โœจ No commits found to process!");
405        }
406
407        Ok(())
408    }
409
410    /// Execute twiddle command with automatic batching for large commit ranges
411    async fn execute_with_batching(
412        &self,
413        use_contextual: bool,
414        full_repo_view: crate::data::RepositoryView,
415    ) -> Result<()> {
416        use crate::data::amendments::AmendmentFile;
417
418        // Initialize Claude client
419        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
420
421        // Show model information
422        self.show_model_info_from_client(&claude_client)?;
423
424        // Split commits into batches
425        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
426
427        let total_batches = commit_batches.len();
428        let mut all_amendments = AmendmentFile {
429            amendments: Vec::new(),
430        };
431
432        println!("๐Ÿ“Š Processing {} batches...", total_batches);
433
434        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
435            println!(
436                "๐Ÿ”„ Processing batch {}/{} ({} commits)...",
437                batch_num + 1,
438                total_batches,
439                commit_batch.len()
440            );
441
442            // Create a repository view for just this batch
443            let batch_repo_view = crate::data::RepositoryView {
444                versions: full_repo_view.versions.clone(),
445                explanation: full_repo_view.explanation.clone(),
446                working_directory: full_repo_view.working_directory.clone(),
447                remotes: full_repo_view.remotes.clone(),
448                ai: full_repo_view.ai.clone(),
449                branch_info: full_repo_view.branch_info.clone(),
450                pr_template: full_repo_view.pr_template.clone(),
451                pr_template_location: full_repo_view.pr_template_location.clone(),
452                branch_prs: full_repo_view.branch_prs.clone(),
453                commits: commit_batch.to_vec(),
454            };
455
456            // Collect context for this batch if needed
457            let batch_context = if use_contextual {
458                Some(self.collect_context(&batch_repo_view).await?)
459            } else {
460                None
461            };
462
463            // Generate amendments for this batch
464            let batch_amendments = if let Some(ctx) = batch_context {
465                claude_client
466                    .generate_contextual_amendments(&batch_repo_view, &ctx)
467                    .await?
468            } else {
469                claude_client.generate_amendments(&batch_repo_view).await?
470            };
471
472            // Merge amendments from this batch
473            all_amendments
474                .amendments
475                .extend(batch_amendments.amendments);
476
477            if batch_num + 1 < total_batches {
478                println!("   โœ… Batch {}/{} completed", batch_num + 1, total_batches);
479                // Small delay between batches to be respectful to the API
480                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
481            }
482        }
483
484        println!(
485            "โœ… All batches completed! Found {} commits to improve.",
486            all_amendments.amendments.len()
487        );
488
489        // Handle different output modes
490        if let Some(save_path) = &self.save_only {
491            all_amendments.save_to_file(save_path)?;
492            println!("๐Ÿ’พ Amendments saved to file");
493            return Ok(());
494        }
495
496        // Handle amendments
497        if !all_amendments.amendments.is_empty() {
498            // Create temporary file for amendments
499            let temp_dir = tempfile::tempdir()?;
500            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
501            all_amendments.save_to_file(&amendments_file)?;
502
503            // Show file path and get user choice
504            if !self.auto_apply
505                && !self.handle_amendments_file(&amendments_file, &all_amendments)?
506            {
507                println!("โŒ Amendment cancelled by user");
508                return Ok(());
509            }
510
511            // Apply all amendments (re-read from file to capture any user edits)
512            self.apply_amendments_from_file(&amendments_file).await?;
513            println!("โœ… Commit messages improved successfully!");
514        } else {
515            println!("โœจ No commits found to process!");
516        }
517
518        Ok(())
519    }
520
521    /// Generate repository view (reuse ViewCommand logic)
522    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
523        use crate::data::{
524            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
525            WorkingDirectoryInfo,
526        };
527        use crate::git::{GitRepository, RemoteInfo};
528        use crate::utils::ai_scratch;
529
530        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD~5..HEAD");
531
532        // Open git repository
533        let repo = GitRepository::open()
534            .context("Failed to open git repository. Make sure you're in a git repository.")?;
535
536        // Get current branch name
537        let current_branch = repo
538            .get_current_branch()
539            .unwrap_or_else(|_| "HEAD".to_string());
540
541        // Get working directory status
542        let wd_status = repo.get_working_directory_status()?;
543        let working_directory = WorkingDirectoryInfo {
544            clean: wd_status.clean,
545            untracked_changes: wd_status
546                .untracked_changes
547                .into_iter()
548                .map(|fs| FileStatusInfo {
549                    status: fs.status,
550                    file: fs.file,
551                })
552                .collect(),
553        };
554
555        // Get remote information
556        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
557
558        // Parse commit range and get commits
559        let commits = repo.get_commits_in_range(commit_range)?;
560
561        // Create version information
562        let versions = Some(VersionInfo {
563            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
564        });
565
566        // Get AI scratch directory
567        let ai_scratch_path =
568            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
569        let ai_info = AiInfo {
570            scratch: ai_scratch_path.to_string_lossy().to_string(),
571        };
572
573        // Build repository view with branch info
574        let mut repo_view = RepositoryView {
575            versions,
576            explanation: FieldExplanation::default(),
577            working_directory,
578            remotes,
579            ai: ai_info,
580            branch_info: Some(BranchInfo {
581                branch: current_branch,
582            }),
583            pr_template: None,
584            pr_template_location: None,
585            branch_prs: None,
586            commits,
587        };
588
589        // Update field presence based on actual data
590        repo_view.update_field_presence();
591
592        Ok(repo_view)
593    }
594
595    /// Handle amendments file - show path and get user choice
596    fn handle_amendments_file(
597        &self,
598        amendments_file: &std::path::Path,
599        amendments: &crate::data::amendments::AmendmentFile,
600    ) -> Result<bool> {
601        use std::io::{self, Write};
602
603        println!(
604            "\n๐Ÿ“ Found {} commits that could be improved.",
605            amendments.amendments.len()
606        );
607        println!("๐Ÿ’พ Amendments saved to: {}", amendments_file.display());
608        println!();
609
610        loop {
611            print!("โ“ [A]pply amendments, [S]how file, [E]dit file, or [Q]uit? [A/s/e/q] ");
612            io::stdout().flush()?;
613
614            let mut input = String::new();
615            io::stdin().read_line(&mut input)?;
616
617            match input.trim().to_lowercase().as_str() {
618                "a" | "apply" | "" => return Ok(true),
619                "s" | "show" => {
620                    self.show_amendments_file(amendments_file)?;
621                    println!();
622                }
623                "e" | "edit" => {
624                    self.edit_amendments_file(amendments_file)?;
625                    println!();
626                }
627                "q" | "quit" => return Ok(false),
628                _ => {
629                    println!(
630                        "Invalid choice. Please enter 'a' to apply, 's' to show, 'e' to edit, or 'q' to quit."
631                    );
632                }
633            }
634        }
635    }
636
637    /// Show the contents of the amendments file
638    fn show_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
639        use std::fs;
640
641        println!("\n๐Ÿ“„ Amendments file contents:");
642        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
643
644        let contents =
645            fs::read_to_string(amendments_file).context("Failed to read amendments file")?;
646
647        println!("{}", contents);
648        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
649
650        Ok(())
651    }
652
653    /// Open the amendments file in an external editor
654    fn edit_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
655        use std::env;
656        use std::io::{self, Write};
657        use std::process::Command;
658
659        // Try to get editor from environment variables
660        let editor = env::var("OMNI_DEV_EDITOR")
661            .or_else(|_| env::var("EDITOR"))
662            .unwrap_or_else(|_| {
663                // Prompt user for editor if neither environment variable is set
664                println!(
665                    "๐Ÿ”ง Neither OMNI_DEV_EDITOR nor EDITOR environment variables are defined."
666                );
667                print!("Please enter the command to use as your editor: ");
668                io::stdout().flush().expect("Failed to flush stdout");
669
670                let mut input = String::new();
671                io::stdin()
672                    .read_line(&mut input)
673                    .expect("Failed to read user input");
674                input.trim().to_string()
675            });
676
677        if editor.is_empty() {
678            println!("โŒ No editor specified. Returning to menu.");
679            return Ok(());
680        }
681
682        println!("๐Ÿ“ Opening amendments file in editor: {}", editor);
683
684        // Split editor command to handle arguments
685        let mut cmd_parts = editor.split_whitespace();
686        let editor_cmd = cmd_parts.next().unwrap_or(&editor);
687        let args: Vec<&str> = cmd_parts.collect();
688
689        let mut command = Command::new(editor_cmd);
690        command.args(args);
691        command.arg(amendments_file.to_string_lossy().as_ref());
692
693        match command.status() {
694            Ok(status) => {
695                if status.success() {
696                    println!("โœ… Editor session completed.");
697                } else {
698                    println!(
699                        "โš ๏ธ  Editor exited with non-zero status: {:?}",
700                        status.code()
701                    );
702                }
703            }
704            Err(e) => {
705                println!("โŒ Failed to execute editor '{}': {}", editor, e);
706                println!("   Please check that the editor command is correct and available in your PATH.");
707            }
708        }
709
710        Ok(())
711    }
712
713    /// Apply amendments from a file path (re-reads from disk to capture user edits)
714    async fn apply_amendments_from_file(&self, amendments_file: &std::path::Path) -> Result<()> {
715        use crate::git::AmendmentHandler;
716
717        // Use AmendmentHandler to apply amendments directly from file
718        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
719        handler
720            .apply_amendments(&amendments_file.to_string_lossy())
721            .context("Failed to apply amendments")?;
722
723        Ok(())
724    }
725
726    /// Collect contextual information for enhanced commit message generation
727    async fn collect_context(
728        &self,
729        repo_view: &crate::data::RepositoryView,
730    ) -> Result<crate::data::context::CommitContext> {
731        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
732        use crate::data::context::CommitContext;
733
734        let mut context = CommitContext::new();
735
736        // 1. Discover project context
737        let context_dir = self
738            .context_dir
739            .as_ref()
740            .cloned()
741            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
742
743        // ProjectDiscovery takes repo root and context directory
744        let repo_root = std::path::PathBuf::from(".");
745        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
746        debug!(context_dir = ?context_dir, "Using context directory");
747        match discovery.discover() {
748            Ok(project_context) => {
749                debug!("Discovery successful");
750
751                // Show diagnostic information about loaded guidance files
752                self.show_guidance_files_status(&project_context, &context_dir)?;
753
754                context.project = project_context;
755            }
756            Err(e) => {
757                debug!(error = %e, "Discovery failed");
758                context.project = Default::default();
759            }
760        }
761
762        // 2. Analyze current branch from repository view
763        if let Some(branch_info) = &repo_view.branch_info {
764            context.branch = BranchAnalyzer::analyze(&branch_info.branch).unwrap_or_default();
765        } else {
766            // Fallback to getting current branch directly if not in repo view
767            use crate::git::GitRepository;
768            let repo = GitRepository::open()?;
769            let current_branch = repo
770                .get_current_branch()
771                .unwrap_or_else(|_| "HEAD".to_string());
772            context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
773        }
774
775        // 3. Analyze commit range patterns
776        if !repo_view.commits.is_empty() {
777            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
778        }
779
780        // 4. Apply user-provided context overrides
781        if let Some(ref work_ctx) = self.work_context {
782            context.user_provided = Some(work_ctx.clone());
783        }
784
785        if let Some(ref branch_ctx) = self.branch_context {
786            context.branch.description = branch_ctx.clone();
787        }
788
789        Ok(context)
790    }
791
792    /// Show context summary to user
793    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
794        use crate::data::context::{VerbosityLevel, WorkPattern};
795
796        println!("๐Ÿ” Context Analysis:");
797
798        // Project context
799        if !context.project.valid_scopes.is_empty() {
800            let scope_names: Vec<&str> = context
801                .project
802                .valid_scopes
803                .iter()
804                .map(|s| s.name.as_str())
805                .collect();
806            println!("   ๐Ÿ“ Valid scopes: {}", scope_names.join(", "));
807        }
808
809        // Branch context
810        if context.branch.is_feature_branch {
811            println!(
812                "   ๐ŸŒฟ Branch: {} ({})",
813                context.branch.description, context.branch.work_type
814            );
815            if let Some(ref ticket) = context.branch.ticket_id {
816                println!("   ๐ŸŽซ Ticket: {}", ticket);
817            }
818        }
819
820        // Work pattern
821        match context.range.work_pattern {
822            WorkPattern::Sequential => println!("   ๐Ÿ”„ Pattern: Sequential development"),
823            WorkPattern::Refactoring => println!("   ๐Ÿงน Pattern: Refactoring work"),
824            WorkPattern::BugHunt => println!("   ๐Ÿ› Pattern: Bug investigation"),
825            WorkPattern::Documentation => println!("   ๐Ÿ“– Pattern: Documentation updates"),
826            WorkPattern::Configuration => println!("   โš™๏ธ  Pattern: Configuration changes"),
827            WorkPattern::Unknown => {}
828        }
829
830        // Verbosity level
831        match context.suggested_verbosity() {
832            VerbosityLevel::Comprehensive => {
833                println!("   ๐Ÿ“ Detail level: Comprehensive (significant changes detected)")
834            }
835            VerbosityLevel::Detailed => println!("   ๐Ÿ“ Detail level: Detailed"),
836            VerbosityLevel::Concise => println!("   ๐Ÿ“ Detail level: Concise"),
837        }
838
839        // User context
840        if let Some(ref user_ctx) = context.user_provided {
841            println!("   ๐Ÿ‘ค User context: {}", user_ctx);
842        }
843
844        println!();
845        Ok(())
846    }
847
848    /// Show model information from actual AI client
849    fn show_model_info_from_client(
850        &self,
851        client: &crate::claude::client::ClaudeClient,
852    ) -> Result<()> {
853        use crate::claude::model_config::get_model_registry;
854
855        println!("๐Ÿค– AI Model Configuration:");
856
857        // Get actual metadata from the client
858        let metadata = client.get_ai_client_metadata();
859        let registry = get_model_registry();
860
861        if let Some(spec) = registry.get_model_spec(&metadata.model) {
862            // Highlight the API identifier portion in yellow
863            if metadata.model != spec.api_identifier {
864                println!(
865                    "   ๐Ÿ“ก Model: {} โ†’ \x1b[33m{}\x1b[0m",
866                    metadata.model, spec.api_identifier
867                );
868            } else {
869                println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", metadata.model);
870            }
871
872            println!("   ๐Ÿท๏ธ  Provider: {}", spec.provider);
873            println!("   ๐Ÿ“Š Generation: {}", spec.generation);
874            println!("   โญ Tier: {} ({})", spec.tier, {
875                if let Some(tier_info) = registry.get_tier_info(&spec.provider, &spec.tier) {
876                    &tier_info.description
877                } else {
878                    "No description available"
879                }
880            });
881            println!("   ๐Ÿ“ค Max output tokens: {}", spec.max_output_tokens);
882            println!("   ๐Ÿ“ฅ Input context: {}", spec.input_context);
883
884            if spec.legacy {
885                println!("   โš ๏ธ  Legacy model (consider upgrading to newer version)");
886            }
887        } else {
888            // Fallback to client metadata if not in registry
889            println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", metadata.model);
890            println!("   ๐Ÿท๏ธ  Provider: {}", metadata.provider);
891            println!("   โš ๏ธ  Model not found in registry, using client metadata:");
892            println!("   ๐Ÿ“ค Max output tokens: {}", metadata.max_response_length);
893            println!("   ๐Ÿ“ฅ Input context: {}", metadata.max_context_length);
894        }
895
896        println!();
897        Ok(())
898    }
899
900    /// Show diagnostic information about loaded guidance files
901    fn show_guidance_files_status(
902        &self,
903        project_context: &crate::data::context::ProjectContext,
904        context_dir: &std::path::Path,
905    ) -> Result<()> {
906        println!("๐Ÿ“‹ Project guidance files status:");
907
908        // Check commit guidelines
909        let guidelines_found = project_context.commit_guidelines.is_some();
910        let guidelines_source = if guidelines_found {
911            let local_path = context_dir.join("local").join("commit-guidelines.md");
912            let project_path = context_dir.join("commit-guidelines.md");
913            let home_path = dirs::home_dir()
914                .map(|h| h.join(".omni-dev").join("commit-guidelines.md"))
915                .unwrap_or_default();
916
917            if local_path.exists() {
918                format!("โœ… Local override: {}", local_path.display())
919            } else if project_path.exists() {
920                format!("โœ… Project: {}", project_path.display())
921            } else if home_path.exists() {
922                format!("โœ… Global: {}", home_path.display())
923            } else {
924                "โœ… (source unknown)".to_string()
925            }
926        } else {
927            "โŒ None found".to_string()
928        };
929        println!("   ๐Ÿ“ Commit guidelines: {}", guidelines_source);
930
931        // Check scopes
932        let scopes_count = project_context.valid_scopes.len();
933        let scopes_source = if scopes_count > 0 {
934            let local_path = context_dir.join("local").join("scopes.yaml");
935            let project_path = context_dir.join("scopes.yaml");
936            let home_path = dirs::home_dir()
937                .map(|h| h.join(".omni-dev").join("scopes.yaml"))
938                .unwrap_or_default();
939
940            let source = if local_path.exists() {
941                format!("Local override: {}", local_path.display())
942            } else if project_path.exists() {
943                format!("Project: {}", project_path.display())
944            } else if home_path.exists() {
945                format!("Global: {}", home_path.display())
946            } else {
947                "(source unknown + ecosystem defaults)".to_string()
948            };
949            format!("โœ… {} ({} scopes)", source, scopes_count)
950        } else {
951            "โŒ None found".to_string()
952        };
953        println!("   ๐ŸŽฏ Valid scopes: {}", scopes_source);
954
955        println!();
956        Ok(())
957    }
958
959    /// Execute twiddle command without AI - create amendments with original messages
960    async fn execute_no_ai(&self) -> Result<()> {
961        use crate::data::amendments::{Amendment, AmendmentFile};
962
963        println!("๐Ÿ“‹ Generating amendments YAML without AI processing...");
964
965        // Generate repository view to get all commits
966        let repo_view = self.generate_repository_view().await?;
967
968        // Create amendments with original commit messages (no AI improvements)
969        let amendments: Vec<Amendment> = repo_view
970            .commits
971            .iter()
972            .map(|commit| Amendment {
973                commit: commit.hash.clone(),
974                message: commit.original_message.clone(),
975            })
976            .collect();
977
978        let amendment_file = AmendmentFile { amendments };
979
980        // Handle different output modes
981        if let Some(save_path) = &self.save_only {
982            amendment_file.save_to_file(save_path)?;
983            println!("๐Ÿ’พ Amendments saved to file");
984            return Ok(());
985        }
986
987        // Handle amendments using the same flow as the AI-powered version
988        if !amendment_file.amendments.is_empty() {
989            // Create temporary file for amendments
990            let temp_dir = tempfile::tempdir()?;
991            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
992            amendment_file.save_to_file(&amendments_file)?;
993
994            // Show file path and get user choice
995            if !self.auto_apply
996                && !self.handle_amendments_file(&amendments_file, &amendment_file)?
997            {
998                println!("โŒ Amendment cancelled by user");
999                return Ok(());
1000            }
1001
1002            // Apply amendments (re-read from file to capture any user edits)
1003            self.apply_amendments_from_file(&amendments_file).await?;
1004            println!("โœ… Commit messages applied successfully!");
1005        } else {
1006            println!("โœจ No commits found to process!");
1007        }
1008
1009        Ok(())
1010    }
1011}
1012
1013impl BranchCommand {
1014    /// Execute branch command
1015    pub fn execute(self) -> Result<()> {
1016        match self.command {
1017            BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
1018            BranchSubcommands::Create(create_cmd) => {
1019                // Use tokio runtime for async execution
1020                let rt = tokio::runtime::Runtime::new()
1021                    .context("Failed to create tokio runtime for PR creation")?;
1022                rt.block_on(create_cmd.execute())
1023            }
1024        }
1025    }
1026}
1027
1028impl InfoCommand {
1029    /// Execute info command
1030    pub fn execute(self) -> Result<()> {
1031        use crate::data::{
1032            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
1033            WorkingDirectoryInfo,
1034        };
1035        use crate::git::{GitRepository, RemoteInfo};
1036        use crate::utils::ai_scratch;
1037
1038        // Open git repository
1039        let repo = GitRepository::open()
1040            .context("Failed to open git repository. Make sure you're in a git repository.")?;
1041
1042        // Get current branch name
1043        let current_branch = repo.get_current_branch().context(
1044            "Failed to get current branch. Make sure you're not in detached HEAD state.",
1045        )?;
1046
1047        // Determine base branch
1048        let base_branch = match self.base_branch {
1049            Some(branch) => {
1050                // Validate that the specified base branch exists
1051                if !repo.branch_exists(&branch)? {
1052                    anyhow::bail!("Base branch '{}' does not exist", branch);
1053                }
1054                branch
1055            }
1056            None => {
1057                // Default to main or master
1058                if repo.branch_exists("main")? {
1059                    "main".to_string()
1060                } else if repo.branch_exists("master")? {
1061                    "master".to_string()
1062                } else {
1063                    anyhow::bail!("No default base branch found (main or master)");
1064                }
1065            }
1066        };
1067
1068        // Calculate commit range: [base_branch]..HEAD
1069        let commit_range = format!("{}..HEAD", base_branch);
1070
1071        // Get working directory status
1072        let wd_status = repo.get_working_directory_status()?;
1073        let working_directory = WorkingDirectoryInfo {
1074            clean: wd_status.clean,
1075            untracked_changes: wd_status
1076                .untracked_changes
1077                .into_iter()
1078                .map(|fs| FileStatusInfo {
1079                    status: fs.status,
1080                    file: fs.file,
1081                })
1082                .collect(),
1083        };
1084
1085        // Get remote information
1086        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1087
1088        // Parse commit range and get commits
1089        let commits = repo.get_commits_in_range(&commit_range)?;
1090
1091        // Check for PR template
1092        let pr_template_result = Self::read_pr_template().ok();
1093        let (pr_template, pr_template_location) = match pr_template_result {
1094            Some((content, location)) => (Some(content), Some(location)),
1095            None => (None, None),
1096        };
1097
1098        // Get PRs for current branch
1099        let branch_prs = Self::get_branch_prs(&current_branch)
1100            .ok()
1101            .filter(|prs| !prs.is_empty());
1102
1103        // Create version information
1104        let versions = Some(VersionInfo {
1105            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
1106        });
1107
1108        // Get AI scratch directory
1109        let ai_scratch_path =
1110            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
1111        let ai_info = AiInfo {
1112            scratch: ai_scratch_path.to_string_lossy().to_string(),
1113        };
1114
1115        // Build repository view with branch info
1116        let mut repo_view = RepositoryView {
1117            versions,
1118            explanation: FieldExplanation::default(),
1119            working_directory,
1120            remotes,
1121            ai: ai_info,
1122            branch_info: Some(BranchInfo {
1123                branch: current_branch,
1124            }),
1125            pr_template,
1126            pr_template_location,
1127            branch_prs,
1128            commits,
1129        };
1130
1131        // Update field presence based on actual data
1132        repo_view.update_field_presence();
1133
1134        // Output as YAML
1135        let yaml_output = crate::data::to_yaml(&repo_view)?;
1136        println!("{}", yaml_output);
1137
1138        Ok(())
1139    }
1140
1141    /// Read PR template file if it exists, returning both content and location
1142    fn read_pr_template() -> Result<(String, String)> {
1143        use std::fs;
1144        use std::path::Path;
1145
1146        let template_path = Path::new(".github/pull_request_template.md");
1147        if template_path.exists() {
1148            let content = fs::read_to_string(template_path)
1149                .context("Failed to read .github/pull_request_template.md")?;
1150            Ok((content, template_path.to_string_lossy().to_string()))
1151        } else {
1152            anyhow::bail!("PR template file does not exist")
1153        }
1154    }
1155
1156    /// Get pull requests for the current branch using gh CLI
1157    fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
1158        use serde_json::Value;
1159        use std::process::Command;
1160
1161        // Use gh CLI to get PRs for the branch
1162        let output = Command::new("gh")
1163            .args([
1164                "pr",
1165                "list",
1166                "--head",
1167                branch_name,
1168                "--json",
1169                "number,title,state,url,body",
1170                "--limit",
1171                "50",
1172            ])
1173            .output()
1174            .context("Failed to execute gh command")?;
1175
1176        if !output.status.success() {
1177            anyhow::bail!(
1178                "gh command failed: {}",
1179                String::from_utf8_lossy(&output.stderr)
1180            );
1181        }
1182
1183        let json_str = String::from_utf8_lossy(&output.stdout);
1184        let prs_json: Value =
1185            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
1186
1187        let mut prs = Vec::new();
1188        if let Some(prs_array) = prs_json.as_array() {
1189            for pr_json in prs_array {
1190                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
1191                    pr_json.get("number").and_then(|n| n.as_u64()),
1192                    pr_json.get("title").and_then(|t| t.as_str()),
1193                    pr_json.get("state").and_then(|s| s.as_str()),
1194                    pr_json.get("url").and_then(|u| u.as_str()),
1195                    pr_json.get("body").and_then(|b| b.as_str()),
1196                ) {
1197                    prs.push(crate::data::PullRequest {
1198                        number,
1199                        title: title.to_string(),
1200                        state: state.to_string(),
1201                        url: url.to_string(),
1202                        body: body.to_string(),
1203                    });
1204                }
1205            }
1206        }
1207
1208        Ok(prs)
1209    }
1210}
1211
1212/// PR action choices
1213#[derive(Debug, PartialEq)]
1214enum PrAction {
1215    CreateNew,
1216    UpdateExisting,
1217    Cancel,
1218}
1219
1220/// AI-generated PR content with structured fields
1221#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1222pub struct PrContent {
1223    /// Concise PR title (ideally 50-80 characters)
1224    pub title: String,
1225    /// Full PR description in markdown format
1226    pub description: String,
1227}
1228
1229impl CreateCommand {
1230    /// Execute create command
1231    pub async fn execute(self) -> Result<()> {
1232        match self.command {
1233            CreateSubcommands::Pr(pr_cmd) => pr_cmd.execute().await,
1234        }
1235    }
1236}
1237
1238impl CreatePrCommand {
1239    /// Determine if PR should be created as draft
1240    ///
1241    /// Priority order:
1242    /// 1. --ready flag (not draft)
1243    /// 2. --draft flag (draft)
1244    /// 3. OMNI_DEV_DEFAULT_DRAFT_PR env/config setting
1245    /// 4. Hard-coded default (draft)
1246    fn should_create_as_draft(&self) -> bool {
1247        use crate::utils::settings::get_env_var;
1248
1249        // Explicit flags take precedence
1250        if self.ready {
1251            return false;
1252        }
1253        if self.draft {
1254            return true;
1255        }
1256
1257        // Check configuration setting
1258        get_env_var("OMNI_DEV_DEFAULT_DRAFT_PR")
1259            .ok()
1260            .and_then(|val| match val.to_lowercase().as_str() {
1261                "true" | "1" | "yes" => Some(true),
1262                "false" | "0" | "no" => Some(false),
1263                _ => None,
1264            })
1265            .unwrap_or(true) // Default to draft if not configured
1266    }
1267
1268    /// Execute create PR command
1269    pub async fn execute(self) -> Result<()> {
1270        println!("๐Ÿ”„ Starting pull request creation process...");
1271
1272        // 1. Generate repository view (reuse InfoCommand logic)
1273        let repo_view = self.generate_repository_view()?;
1274
1275        // 2. Validate branch state (always needed)
1276        self.validate_branch_state(&repo_view)?;
1277
1278        // 3. Show guidance files status early (before AI processing)
1279        use crate::claude::context::ProjectDiscovery;
1280        let repo_root = std::path::PathBuf::from(".");
1281        let context_dir = std::path::PathBuf::from(".omni-dev");
1282        let discovery = ProjectDiscovery::new(repo_root, context_dir);
1283        let project_context = discovery.discover().unwrap_or_default();
1284        self.show_guidance_files_status(&project_context)?;
1285
1286        // 4. Show AI model configuration before generation
1287        let claude_client = crate::claude::create_default_claude_client(None)?;
1288        self.show_model_info_from_client(&claude_client)?;
1289
1290        // 5. Show branch analysis and commit information
1291        self.show_commit_range_info(&repo_view)?;
1292
1293        // 6. Show context analysis (quick collection for display only)
1294        let context = {
1295            use crate::claude::context::{BranchAnalyzer, WorkPatternAnalyzer};
1296            use crate::data::context::CommitContext;
1297            let mut context = CommitContext::new();
1298            context.project = project_context;
1299
1300            // Quick analysis for display
1301            if let Some(branch_info) = &repo_view.branch_info {
1302                context.branch = BranchAnalyzer::analyze(&branch_info.branch).unwrap_or_default();
1303            }
1304
1305            if !repo_view.commits.is_empty() {
1306                context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
1307            }
1308            context
1309        };
1310        self.show_context_summary(&context)?;
1311
1312        // 7. Generate AI-powered PR content (title + description)
1313        debug!("About to generate PR content from AI");
1314        let (pr_content, _claude_client) = self
1315            .generate_pr_content_with_client_internal(&repo_view, claude_client)
1316            .await?;
1317
1318        // 8. Show detailed context information (like twiddle command)
1319        self.show_context_information(&repo_view).await?;
1320        debug!(
1321            generated_title = %pr_content.title,
1322            generated_description_length = pr_content.description.len(),
1323            generated_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1324            "Generated PR content from AI"
1325        );
1326
1327        // 5. Handle different output modes
1328        if let Some(save_path) = self.save_only {
1329            let pr_yaml = crate::data::to_yaml(&pr_content)
1330                .context("Failed to serialize PR content to YAML")?;
1331            std::fs::write(&save_path, &pr_yaml).context("Failed to save PR details to file")?;
1332            println!("๐Ÿ’พ PR details saved to: {}", save_path);
1333            return Ok(());
1334        }
1335
1336        // 6. Create temporary file for PR details
1337        debug!("About to serialize PR content to YAML");
1338        let temp_dir = tempfile::tempdir()?;
1339        let pr_file = temp_dir.path().join("pr-details.yaml");
1340
1341        debug!(
1342            pre_serialize_title = %pr_content.title,
1343            pre_serialize_description_length = pr_content.description.len(),
1344            pre_serialize_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1345            "About to serialize PR content with to_yaml"
1346        );
1347
1348        let pr_yaml =
1349            crate::data::to_yaml(&pr_content).context("Failed to serialize PR content to YAML")?;
1350
1351        debug!(
1352            file_path = %pr_file.display(),
1353            yaml_content_length = pr_yaml.len(),
1354            yaml_content = %pr_yaml,
1355            original_title = %pr_content.title,
1356            original_description_length = pr_content.description.len(),
1357            "Writing PR details to temporary YAML file"
1358        );
1359
1360        std::fs::write(&pr_file, &pr_yaml)?;
1361
1362        // 7. Handle PR details file - show path and get user choice
1363        let pr_action = if self.auto_apply {
1364            // For auto-apply, default to update if PR exists, otherwise create new
1365            if repo_view
1366                .branch_prs
1367                .as_ref()
1368                .is_some_and(|prs| !prs.is_empty())
1369            {
1370                PrAction::UpdateExisting
1371            } else {
1372                PrAction::CreateNew
1373            }
1374        } else {
1375            self.handle_pr_file(&pr_file, &repo_view)?
1376        };
1377
1378        if pr_action == PrAction::Cancel {
1379            println!("โŒ PR operation cancelled by user");
1380            return Ok(());
1381        }
1382
1383        // 8. Validate environment only when we're about to create the PR
1384        self.validate_environment()?;
1385
1386        // 9. Create or update PR (re-read from file to capture any user edits)
1387        let final_pr_yaml =
1388            std::fs::read_to_string(&pr_file).context("Failed to read PR details file")?;
1389
1390        debug!(
1391            yaml_length = final_pr_yaml.len(),
1392            yaml_content = %final_pr_yaml,
1393            "Read PR details YAML from file"
1394        );
1395
1396        let final_pr_content: PrContent = serde_yaml::from_str(&final_pr_yaml)
1397            .context("Failed to parse PR details YAML. Please check the file format.")?;
1398
1399        debug!(
1400            title = %final_pr_content.title,
1401            description_length = final_pr_content.description.len(),
1402            description_preview = %final_pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1403            "Parsed PR content from YAML"
1404        );
1405
1406        // Determine draft status
1407        let is_draft = self.should_create_as_draft();
1408
1409        match pr_action {
1410            PrAction::CreateNew => {
1411                self.create_github_pr(
1412                    &repo_view,
1413                    &final_pr_content.title,
1414                    &final_pr_content.description,
1415                    is_draft,
1416                )?;
1417                println!("โœ… Pull request created successfully!");
1418            }
1419            PrAction::UpdateExisting => {
1420                self.update_github_pr(
1421                    &repo_view,
1422                    &final_pr_content.title,
1423                    &final_pr_content.description,
1424                )?;
1425                println!("โœ… Pull request updated successfully!");
1426            }
1427            PrAction::Cancel => unreachable!(), // Already handled above
1428        }
1429
1430        Ok(())
1431    }
1432
1433    /// Validate environment and dependencies
1434    fn validate_environment(&self) -> Result<()> {
1435        // Check if gh CLI is available
1436        let gh_check = std::process::Command::new("gh")
1437            .args(["--version"])
1438            .output();
1439
1440        match gh_check {
1441            Ok(output) if output.status.success() => {
1442                // Test if gh can access the current repo (this validates both auth and repo access)
1443                let repo_check = std::process::Command::new("gh")
1444                    .args(["repo", "view", "--json", "name"])
1445                    .output();
1446
1447                match repo_check {
1448                    Ok(repo_output) if repo_output.status.success() => Ok(()),
1449                    Ok(repo_output) => {
1450                        // Get more specific error from stderr
1451                        let error_details = String::from_utf8_lossy(&repo_output.stderr);
1452                        if error_details.contains("authentication") || error_details.contains("login") {
1453                            anyhow::bail!("GitHub CLI (gh) authentication failed. Please run 'gh auth login' or check your GITHUB_TOKEN environment variable.")
1454                        } else {
1455                            anyhow::bail!("GitHub CLI (gh) cannot access this repository. Error: {}", error_details.trim())
1456                        }
1457                    }
1458                    Err(e) => anyhow::bail!("Failed to test GitHub CLI access: {}", e),
1459                }
1460            }
1461            _ => anyhow::bail!("GitHub CLI (gh) is not installed or not available in PATH. Please install it from https://cli.github.com/"),
1462        }
1463    }
1464
1465    /// Generate repository view (reuse InfoCommand logic)
1466    fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
1467        use crate::data::{
1468            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
1469            WorkingDirectoryInfo,
1470        };
1471        use crate::git::{GitRepository, RemoteInfo};
1472        use crate::utils::ai_scratch;
1473
1474        // Open git repository
1475        let repo = GitRepository::open()
1476            .context("Failed to open git repository. Make sure you're in a git repository.")?;
1477
1478        // Get current branch name
1479        let current_branch = repo.get_current_branch().context(
1480            "Failed to get current branch. Make sure you're not in detached HEAD state.",
1481        )?;
1482
1483        // Get remote information to determine proper remote and main branch
1484        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1485
1486        // Find the primary remote (prefer origin, fallback to first available)
1487        let primary_remote = remotes
1488            .iter()
1489            .find(|r| r.name == "origin")
1490            .or_else(|| remotes.first())
1491            .ok_or_else(|| anyhow::anyhow!("No remotes found in repository"))?;
1492
1493        // Determine base branch (with remote prefix)
1494        let base_branch = match self.base_branch.as_ref() {
1495            Some(branch) => {
1496                // User specified base branch - need to determine if it's local or remote format
1497                let remote_branch = if branch.contains('/') {
1498                    // Already in remote format (e.g., "origin/main")
1499                    branch.clone()
1500                } else {
1501                    // Local branch name - convert to remote format
1502                    format!("{}/{}", primary_remote.name, branch)
1503                };
1504
1505                // Validate that the remote branch exists
1506                let remote_ref = format!("refs/remotes/{}", remote_branch);
1507                if repo.repository().find_reference(&remote_ref).is_err() {
1508                    anyhow::bail!("Remote branch '{}' does not exist", remote_branch);
1509                }
1510                remote_branch
1511            }
1512            None => {
1513                // Auto-detect using the primary remote's main branch
1514                let main_branch = &primary_remote.main_branch;
1515                if main_branch == "unknown" {
1516                    anyhow::bail!(
1517                        "Could not determine main branch for remote '{}'",
1518                        primary_remote.name
1519                    );
1520                }
1521
1522                let remote_main = format!("{}/{}", primary_remote.name, main_branch);
1523
1524                // Validate that the remote main branch exists
1525                let remote_ref = format!("refs/remotes/{}", remote_main);
1526                if repo.repository().find_reference(&remote_ref).is_err() {
1527                    anyhow::bail!(
1528                        "Remote main branch '{}' does not exist. Try running 'git fetch' first.",
1529                        remote_main
1530                    );
1531                }
1532
1533                remote_main
1534            }
1535        };
1536
1537        // Calculate commit range: [remote_base]..HEAD
1538        let commit_range = format!("{}..HEAD", base_branch);
1539
1540        // Get working directory status
1541        let wd_status = repo.get_working_directory_status()?;
1542        let working_directory = WorkingDirectoryInfo {
1543            clean: wd_status.clean,
1544            untracked_changes: wd_status
1545                .untracked_changes
1546                .into_iter()
1547                .map(|fs| FileStatusInfo {
1548                    status: fs.status,
1549                    file: fs.file,
1550                })
1551                .collect(),
1552        };
1553
1554        // Get remote information
1555        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1556
1557        // Parse commit range and get commits
1558        let commits = repo.get_commits_in_range(&commit_range)?;
1559
1560        // Check for PR template
1561        let pr_template_result = InfoCommand::read_pr_template().ok();
1562        let (pr_template, pr_template_location) = match pr_template_result {
1563            Some((content, location)) => (Some(content), Some(location)),
1564            None => (None, None),
1565        };
1566
1567        // Get PRs for current branch
1568        let branch_prs = InfoCommand::get_branch_prs(&current_branch)
1569            .ok()
1570            .filter(|prs| !prs.is_empty());
1571
1572        // Create version information
1573        let versions = Some(VersionInfo {
1574            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
1575        });
1576
1577        // Get AI scratch directory
1578        let ai_scratch_path =
1579            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
1580        let ai_info = AiInfo {
1581            scratch: ai_scratch_path.to_string_lossy().to_string(),
1582        };
1583
1584        // Build repository view with branch info
1585        let mut repo_view = RepositoryView {
1586            versions,
1587            explanation: FieldExplanation::default(),
1588            working_directory,
1589            remotes,
1590            ai: ai_info,
1591            branch_info: Some(BranchInfo {
1592                branch: current_branch,
1593            }),
1594            pr_template,
1595            pr_template_location,
1596            branch_prs,
1597            commits,
1598        };
1599
1600        // Update field presence based on actual data
1601        repo_view.update_field_presence();
1602
1603        Ok(repo_view)
1604    }
1605
1606    /// Validate branch state for PR creation
1607    fn validate_branch_state(&self, repo_view: &crate::data::RepositoryView) -> Result<()> {
1608        // Check if working directory is clean
1609        if !repo_view.working_directory.clean {
1610            anyhow::bail!(
1611                "Working directory has uncommitted changes. Please commit or stash your changes before creating a PR."
1612            );
1613        }
1614
1615        // Check if there are any untracked changes
1616        if !repo_view.working_directory.untracked_changes.is_empty() {
1617            let file_list: Vec<&str> = repo_view
1618                .working_directory
1619                .untracked_changes
1620                .iter()
1621                .map(|f| f.file.as_str())
1622                .collect();
1623            anyhow::bail!(
1624                "Working directory has untracked changes: {}. Please commit or stash your changes before creating a PR.",
1625                file_list.join(", ")
1626            );
1627        }
1628
1629        // Check if commits exist
1630        if repo_view.commits.is_empty() {
1631            anyhow::bail!("No commits found to create PR from. Make sure you have commits that are not in the base branch.");
1632        }
1633
1634        // Check if PR already exists for this branch
1635        if let Some(existing_prs) = &repo_view.branch_prs {
1636            if !existing_prs.is_empty() {
1637                let pr_info: Vec<String> = existing_prs
1638                    .iter()
1639                    .map(|pr| format!("#{} ({})", pr.number, pr.state))
1640                    .collect();
1641
1642                println!(
1643                    "๐Ÿ“‹ Existing PR(s) found for this branch: {}",
1644                    pr_info.join(", ")
1645                );
1646                // Don't bail - we'll handle this in the main flow
1647            }
1648        }
1649
1650        Ok(())
1651    }
1652
1653    /// Show detailed context information (similar to twiddle command)
1654    async fn show_context_information(
1655        &self,
1656        _repo_view: &crate::data::RepositoryView,
1657    ) -> Result<()> {
1658        // Note: commit range info and context summary are now shown earlier
1659        // This method is kept for potential future detailed information
1660        // that should be shown after AI generation
1661
1662        Ok(())
1663    }
1664
1665    /// Show commit range and count information
1666    fn show_commit_range_info(&self, repo_view: &crate::data::RepositoryView) -> Result<()> {
1667        // Recreate the base branch determination logic from generate_repository_view
1668        let base_branch = match self.base_branch.as_ref() {
1669            Some(branch) => {
1670                // User specified base branch
1671                if branch.contains('/') {
1672                    // Already in remote format
1673                    branch.clone()
1674                } else {
1675                    // Get the primary remote name from repo_view
1676                    let primary_remote_name = repo_view
1677                        .remotes
1678                        .iter()
1679                        .find(|r| r.name == "origin")
1680                        .or_else(|| repo_view.remotes.first())
1681                        .map(|r| r.name.as_str())
1682                        .unwrap_or("origin");
1683                    format!("{}/{}", primary_remote_name, branch)
1684                }
1685            }
1686            None => {
1687                // Auto-detected base branch from remotes
1688                repo_view
1689                    .remotes
1690                    .iter()
1691                    .find(|r| r.name == "origin")
1692                    .or_else(|| repo_view.remotes.first())
1693                    .map(|r| format!("{}/{}", r.name, r.main_branch))
1694                    .unwrap_or_else(|| "unknown".to_string())
1695            }
1696        };
1697
1698        let commit_range = format!("{}..HEAD", base_branch);
1699        let commit_count = repo_view.commits.len();
1700
1701        // Get current branch name
1702        let current_branch = repo_view
1703            .branch_info
1704            .as_ref()
1705            .map(|bi| bi.branch.as_str())
1706            .unwrap_or("unknown");
1707
1708        println!("๐Ÿ“Š Branch Analysis:");
1709        println!("   ๐ŸŒฟ Current branch: {}", current_branch);
1710        println!("   ๐Ÿ“ Commit range: {}", commit_range);
1711        println!("   ๐Ÿ“ Commits found: {} commits", commit_count);
1712        println!();
1713
1714        Ok(())
1715    }
1716
1717    /// Collect contextual information for enhanced PR generation (adapted from twiddle)
1718    async fn collect_context(
1719        &self,
1720        repo_view: &crate::data::RepositoryView,
1721    ) -> Result<crate::data::context::CommitContext> {
1722        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
1723        use crate::data::context::CommitContext;
1724        use crate::git::GitRepository;
1725
1726        let mut context = CommitContext::new();
1727
1728        // 1. Discover project context
1729        let context_dir = std::path::PathBuf::from(".omni-dev");
1730
1731        // ProjectDiscovery takes repo root and context directory
1732        let repo_root = std::path::PathBuf::from(".");
1733        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
1734        match discovery.discover() {
1735            Ok(project_context) => {
1736                context.project = project_context;
1737            }
1738            Err(_e) => {
1739                context.project = Default::default();
1740            }
1741        }
1742
1743        // 2. Analyze current branch
1744        let repo = GitRepository::open()?;
1745        let current_branch = repo
1746            .get_current_branch()
1747            .unwrap_or_else(|_| "HEAD".to_string());
1748        context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
1749
1750        // 3. Analyze commit range patterns
1751        if !repo_view.commits.is_empty() {
1752            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
1753        }
1754
1755        Ok(context)
1756    }
1757
1758    /// Show guidance files status (adapted from twiddle)
1759    fn show_guidance_files_status(
1760        &self,
1761        project_context: &crate::data::context::ProjectContext,
1762    ) -> Result<()> {
1763        let context_dir = std::path::PathBuf::from(".omni-dev");
1764
1765        println!("๐Ÿ“‹ Project guidance files status:");
1766
1767        // Check PR guidelines (for PR commands)
1768        let pr_guidelines_found = project_context.pr_guidelines.is_some();
1769        let pr_guidelines_source = if pr_guidelines_found {
1770            let local_path = context_dir.join("local").join("pr-guidelines.md");
1771            let project_path = context_dir.join("pr-guidelines.md");
1772            let home_path = dirs::home_dir()
1773                .map(|h| h.join(".omni-dev").join("pr-guidelines.md"))
1774                .unwrap_or_default();
1775
1776            if local_path.exists() {
1777                format!("โœ… Local override: {}", local_path.display())
1778            } else if project_path.exists() {
1779                format!("โœ… Project: {}", project_path.display())
1780            } else if home_path.exists() {
1781                format!("โœ… Global: {}", home_path.display())
1782            } else {
1783                "โœ… (source unknown)".to_string()
1784            }
1785        } else {
1786            "โŒ None found".to_string()
1787        };
1788        println!("   ๐Ÿ”€ PR guidelines: {}", pr_guidelines_source);
1789
1790        // Check scopes
1791        let scopes_count = project_context.valid_scopes.len();
1792        let scopes_source = if scopes_count > 0 {
1793            let local_path = context_dir.join("local").join("scopes.yaml");
1794            let project_path = context_dir.join("scopes.yaml");
1795            let home_path = dirs::home_dir()
1796                .map(|h| h.join(".omni-dev").join("scopes.yaml"))
1797                .unwrap_or_default();
1798
1799            let source = if local_path.exists() {
1800                format!("Local override: {}", local_path.display())
1801            } else if project_path.exists() {
1802                format!("Project: {}", project_path.display())
1803            } else if home_path.exists() {
1804                format!("Global: {}", home_path.display())
1805            } else {
1806                "(source unknown + ecosystem defaults)".to_string()
1807            };
1808            format!("โœ… {} ({} scopes)", source, scopes_count)
1809        } else {
1810            "โŒ None found".to_string()
1811        };
1812        println!("   ๐ŸŽฏ Valid scopes: {}", scopes_source);
1813
1814        // Check PR template
1815        let pr_template_path = std::path::Path::new(".github/pull_request_template.md");
1816        let pr_template_status = if pr_template_path.exists() {
1817            format!("โœ… Project: {}", pr_template_path.display())
1818        } else {
1819            "โŒ None found".to_string()
1820        };
1821        println!("   ๐Ÿ“‹ PR template: {}", pr_template_status);
1822
1823        println!();
1824        Ok(())
1825    }
1826
1827    /// Show context summary (adapted from twiddle)
1828    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
1829        use crate::data::context::{VerbosityLevel, WorkPattern};
1830
1831        println!("๐Ÿ” Context Analysis:");
1832
1833        // Project context
1834        if !context.project.valid_scopes.is_empty() {
1835            let scope_names: Vec<&str> = context
1836                .project
1837                .valid_scopes
1838                .iter()
1839                .map(|s| s.name.as_str())
1840                .collect();
1841            println!("   ๐Ÿ“ Valid scopes: {}", scope_names.join(", "));
1842        }
1843
1844        // Branch context
1845        if context.branch.is_feature_branch {
1846            println!(
1847                "   ๐ŸŒฟ Branch: {} ({})",
1848                context.branch.description, context.branch.work_type
1849            );
1850            if let Some(ref ticket) = context.branch.ticket_id {
1851                println!("   ๐ŸŽซ Ticket: {}", ticket);
1852            }
1853        }
1854
1855        // Work pattern
1856        match context.range.work_pattern {
1857            WorkPattern::Sequential => println!("   ๐Ÿ”„ Pattern: Sequential development"),
1858            WorkPattern::Refactoring => println!("   ๐Ÿงน Pattern: Refactoring work"),
1859            WorkPattern::BugHunt => println!("   ๐Ÿ› Pattern: Bug investigation"),
1860            WorkPattern::Documentation => println!("   ๐Ÿ“– Pattern: Documentation updates"),
1861            WorkPattern::Configuration => println!("   โš™๏ธ  Pattern: Configuration changes"),
1862            WorkPattern::Unknown => {}
1863        }
1864
1865        // Verbosity level
1866        match context.suggested_verbosity() {
1867            VerbosityLevel::Comprehensive => {
1868                println!("   ๐Ÿ“ Detail level: Comprehensive (significant changes detected)")
1869            }
1870            VerbosityLevel::Detailed => println!("   ๐Ÿ“ Detail level: Detailed"),
1871            VerbosityLevel::Concise => println!("   ๐Ÿ“ Detail level: Concise"),
1872        }
1873
1874        println!();
1875        Ok(())
1876    }
1877
1878    /// Generate PR content with pre-created client (internal method that doesn't show model info)
1879    async fn generate_pr_content_with_client_internal(
1880        &self,
1881        repo_view: &crate::data::RepositoryView,
1882        claude_client: crate::claude::client::ClaudeClient,
1883    ) -> Result<(PrContent, crate::claude::client::ClaudeClient)> {
1884        use tracing::debug;
1885
1886        // Get PR template (either from repo or default)
1887        let pr_template = match &repo_view.pr_template {
1888            Some(template) => template.clone(),
1889            None => self.get_default_pr_template(),
1890        };
1891
1892        debug!(
1893            pr_template_length = pr_template.len(),
1894            pr_template_preview = %pr_template.lines().take(5).collect::<Vec<_>>().join("\\n"),
1895            "Using PR template for generation"
1896        );
1897
1898        println!("๐Ÿค– Generating AI-powered PR description...");
1899
1900        // Collect project context for PR guidelines
1901        debug!("Collecting context for PR generation");
1902        let context = self.collect_context(repo_view).await?;
1903        debug!("Context collection completed");
1904
1905        // Generate AI-powered PR content with context
1906        debug!("About to call Claude AI for PR content generation");
1907        match claude_client
1908            .generate_pr_content_with_context(repo_view, &pr_template, &context)
1909            .await
1910        {
1911            Ok(pr_content) => {
1912                debug!(
1913                    ai_generated_title = %pr_content.title,
1914                    ai_generated_description_length = pr_content.description.len(),
1915                    ai_generated_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1916                    "AI successfully generated PR content"
1917                );
1918                Ok((pr_content, claude_client))
1919            }
1920            Err(e) => {
1921                debug!(error = %e, "AI PR generation failed, falling back to basic description");
1922                // Fallback to basic description with commit analysis (silently)
1923                let mut description = pr_template;
1924                self.enhance_description_with_commits(&mut description, repo_view)?;
1925
1926                // Generate fallback title from commits
1927                let title = self.generate_title_from_commits(repo_view);
1928
1929                debug!(
1930                    fallback_title = %title,
1931                    fallback_description_length = description.len(),
1932                    "Created fallback PR content"
1933                );
1934
1935                Ok((PrContent { title, description }, claude_client))
1936            }
1937        }
1938    }
1939
1940    /// Get default PR template when none exists in the repository
1941    fn get_default_pr_template(&self) -> String {
1942        r#"# Pull Request
1943
1944## Description
1945<!-- Provide a brief description of what this PR does -->
1946
1947## Type of Change
1948<!-- Mark the relevant option with an "x" -->
1949- [ ] Bug fix (non-breaking change which fixes an issue)
1950- [ ] New feature (non-breaking change which adds functionality)
1951- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
1952- [ ] Documentation update
1953- [ ] Refactoring (no functional changes)
1954- [ ] Performance improvement
1955- [ ] Test coverage improvement
1956
1957## Changes Made
1958<!-- List the specific changes made in this PR -->
1959- 
1960- 
1961- 
1962
1963## Testing
1964- [ ] All existing tests pass
1965- [ ] New tests added for new functionality
1966- [ ] Manual testing performed
1967
1968## Additional Notes
1969<!-- Add any additional notes for reviewers -->
1970"#.to_string()
1971    }
1972
1973    /// Enhance PR description with commit analysis
1974    fn enhance_description_with_commits(
1975        &self,
1976        description: &mut String,
1977        repo_view: &crate::data::RepositoryView,
1978    ) -> Result<()> {
1979        if repo_view.commits.is_empty() {
1980            return Ok(());
1981        }
1982
1983        // Add commit summary section
1984        description.push_str("\n---\n");
1985        description.push_str("## ๐Ÿ“ Commit Summary\n");
1986        description
1987            .push_str("*This section was automatically generated based on commit analysis*\n\n");
1988
1989        // Analyze commit types and scopes
1990        let mut types_found = std::collections::HashSet::new();
1991        let mut scopes_found = std::collections::HashSet::new();
1992        let mut has_breaking_changes = false;
1993
1994        for commit in &repo_view.commits {
1995            let detected_type = &commit.analysis.detected_type;
1996            types_found.insert(detected_type.clone());
1997            if detected_type.contains("BREAKING")
1998                || commit.original_message.contains("BREAKING CHANGE")
1999            {
2000                has_breaking_changes = true;
2001            }
2002
2003            let detected_scope = &commit.analysis.detected_scope;
2004            if !detected_scope.is_empty() {
2005                scopes_found.insert(detected_scope.clone());
2006            }
2007        }
2008
2009        // Update type checkboxes based on detected types
2010        if let Some(feat_pos) = description.find("- [ ] New feature") {
2011            if types_found.contains("feat") {
2012                description.replace_range(feat_pos..feat_pos + 5, "- [x]");
2013            }
2014        }
2015        if let Some(fix_pos) = description.find("- [ ] Bug fix") {
2016            if types_found.contains("fix") {
2017                description.replace_range(fix_pos..fix_pos + 5, "- [x]");
2018            }
2019        }
2020        if let Some(docs_pos) = description.find("- [ ] Documentation update") {
2021            if types_found.contains("docs") {
2022                description.replace_range(docs_pos..docs_pos + 5, "- [x]");
2023            }
2024        }
2025        if let Some(refactor_pos) = description.find("- [ ] Refactoring") {
2026            if types_found.contains("refactor") {
2027                description.replace_range(refactor_pos..refactor_pos + 5, "- [x]");
2028            }
2029        }
2030        if let Some(breaking_pos) = description.find("- [ ] Breaking change") {
2031            if has_breaking_changes {
2032                description.replace_range(breaking_pos..breaking_pos + 5, "- [x]");
2033            }
2034        }
2035
2036        // Add detected scopes
2037        if !scopes_found.is_empty() {
2038            let scopes_list: Vec<_> = scopes_found.into_iter().collect();
2039            description.push_str(&format!(
2040                "**Affected areas:** {}\n\n",
2041                scopes_list.join(", ")
2042            ));
2043        }
2044
2045        // Add commit list
2046        description.push_str("### Commits in this PR:\n");
2047        for commit in &repo_view.commits {
2048            let short_hash = &commit.hash[..8];
2049            let first_line = commit.original_message.lines().next().unwrap_or("").trim();
2050            description.push_str(&format!("- `{}` {}\n", short_hash, first_line));
2051        }
2052
2053        // Add file change summary
2054        let total_files: usize = repo_view
2055            .commits
2056            .iter()
2057            .map(|c| c.analysis.file_changes.total_files)
2058            .sum();
2059
2060        if total_files > 0 {
2061            description.push_str(&format!("\n**Files changed:** {} files\n", total_files));
2062        }
2063
2064        Ok(())
2065    }
2066
2067    /// Handle PR description file - show path and get user choice
2068    fn handle_pr_file(
2069        &self,
2070        pr_file: &std::path::Path,
2071        repo_view: &crate::data::RepositoryView,
2072    ) -> Result<PrAction> {
2073        use std::io::{self, Write};
2074
2075        println!("\n๐Ÿ“ PR details generated.");
2076        println!("๐Ÿ’พ Details saved to: {}", pr_file.display());
2077
2078        // Show draft status
2079        let is_draft = self.should_create_as_draft();
2080        let status_icon = if is_draft { "๐Ÿ“‹" } else { "โœ…" };
2081        let status_text = if is_draft {
2082            "draft"
2083        } else {
2084            "ready for review"
2085        };
2086        println!("{} PR will be created as: {}", status_icon, status_text);
2087        println!();
2088
2089        // Check if there are existing PRs and show different options
2090        let has_existing_prs = repo_view
2091            .branch_prs
2092            .as_ref()
2093            .is_some_and(|prs| !prs.is_empty());
2094
2095        loop {
2096            if has_existing_prs {
2097                print!("โ“ [U]pdate existing PR, [N]ew PR anyway, [S]how file, [E]dit file, or [Q]uit? [U/n/s/e/q] ");
2098            } else {
2099                print!(
2100                    "โ“ [A]ccept and create PR, [S]how file, [E]dit file, or [Q]uit? [A/s/e/q] "
2101                );
2102            }
2103            io::stdout().flush()?;
2104
2105            let mut input = String::new();
2106            io::stdin().read_line(&mut input)?;
2107
2108            match input.trim().to_lowercase().as_str() {
2109                "u" | "update" if has_existing_prs => return Ok(PrAction::UpdateExisting),
2110                "n" | "new" if has_existing_prs => return Ok(PrAction::CreateNew),
2111                "a" | "accept" | "" if !has_existing_prs => return Ok(PrAction::CreateNew),
2112                "s" | "show" => {
2113                    self.show_pr_file(pr_file)?;
2114                    println!();
2115                }
2116                "e" | "edit" => {
2117                    self.edit_pr_file(pr_file)?;
2118                    println!();
2119                }
2120                "q" | "quit" => return Ok(PrAction::Cancel),
2121                _ => {
2122                    if has_existing_prs {
2123                        println!("Invalid choice. Please enter 'u' to update existing PR, 'n' for new PR, 's' to show, 'e' to edit, or 'q' to quit.");
2124                    } else {
2125                        println!("Invalid choice. Please enter 'a' to accept, 's' to show, 'e' to edit, or 'q' to quit.");
2126                    }
2127                }
2128            }
2129        }
2130    }
2131
2132    /// Show the contents of the PR details file
2133    fn show_pr_file(&self, pr_file: &std::path::Path) -> Result<()> {
2134        use std::fs;
2135
2136        println!("\n๐Ÿ“„ PR details file contents:");
2137        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
2138
2139        let contents = fs::read_to_string(pr_file).context("Failed to read PR details file")?;
2140        println!("{}", contents);
2141        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
2142
2143        Ok(())
2144    }
2145
2146    /// Open the PR details file in an external editor
2147    fn edit_pr_file(&self, pr_file: &std::path::Path) -> Result<()> {
2148        use std::env;
2149        use std::io::{self, Write};
2150        use std::process::Command;
2151
2152        // Try to get editor from environment variables
2153        let editor = env::var("OMNI_DEV_EDITOR")
2154            .or_else(|_| env::var("EDITOR"))
2155            .unwrap_or_else(|_| {
2156                // Prompt user for editor if neither environment variable is set
2157                println!(
2158                    "๐Ÿ”ง Neither OMNI_DEV_EDITOR nor EDITOR environment variables are defined."
2159                );
2160                print!("Please enter the command to use as your editor: ");
2161                io::stdout().flush().expect("Failed to flush stdout");
2162
2163                let mut input = String::new();
2164                io::stdin()
2165                    .read_line(&mut input)
2166                    .expect("Failed to read user input");
2167                input.trim().to_string()
2168            });
2169
2170        if editor.is_empty() {
2171            println!("โŒ No editor specified. Returning to menu.");
2172            return Ok(());
2173        }
2174
2175        println!("๐Ÿ“ Opening PR details file in editor: {}", editor);
2176
2177        // Split editor command to handle arguments
2178        let mut cmd_parts = editor.split_whitespace();
2179        let editor_cmd = cmd_parts.next().unwrap_or(&editor);
2180        let args: Vec<&str> = cmd_parts.collect();
2181
2182        let mut command = Command::new(editor_cmd);
2183        command.args(args);
2184        command.arg(pr_file.to_string_lossy().as_ref());
2185
2186        match command.status() {
2187            Ok(status) => {
2188                if status.success() {
2189                    println!("โœ… Editor session completed.");
2190                } else {
2191                    println!(
2192                        "โš ๏ธ  Editor exited with non-zero status: {:?}",
2193                        status.code()
2194                    );
2195                }
2196            }
2197            Err(e) => {
2198                println!("โŒ Failed to execute editor '{}': {}", editor, e);
2199                println!("   Please check that the editor command is correct and available in your PATH.");
2200            }
2201        }
2202
2203        Ok(())
2204    }
2205
2206    /// Generate a concise title from commit analysis (fallback)
2207    fn generate_title_from_commits(&self, repo_view: &crate::data::RepositoryView) -> String {
2208        if repo_view.commits.is_empty() {
2209            return "Pull Request".to_string();
2210        }
2211
2212        // For single commit, use its first line
2213        if repo_view.commits.len() == 1 {
2214            return repo_view.commits[0]
2215                .original_message
2216                .lines()
2217                .next()
2218                .unwrap_or("Pull Request")
2219                .trim()
2220                .to_string();
2221        }
2222
2223        // For multiple commits, generate from branch name
2224        let branch_name = repo_view
2225            .branch_info
2226            .as_ref()
2227            .map(|bi| bi.branch.as_str())
2228            .unwrap_or("feature");
2229
2230        let cleaned_branch = branch_name.replace(['/', '-', '_'], " ");
2231
2232        format!("feat: {}", cleaned_branch)
2233    }
2234
2235    /// Create new GitHub PR using gh CLI
2236    fn create_github_pr(
2237        &self,
2238        repo_view: &crate::data::RepositoryView,
2239        title: &str,
2240        description: &str,
2241        is_draft: bool,
2242    ) -> Result<()> {
2243        use std::process::Command;
2244
2245        // Get branch name
2246        let branch_name = repo_view
2247            .branch_info
2248            .as_ref()
2249            .map(|bi| &bi.branch)
2250            .context("Branch info not available")?;
2251
2252        let pr_status = if is_draft {
2253            "draft"
2254        } else {
2255            "ready for review"
2256        };
2257        println!("๐Ÿš€ Creating pull request ({})...", pr_status);
2258        println!("   ๐Ÿ“‹ Title: {}", title);
2259        println!("   ๐ŸŒฟ Branch: {}", branch_name);
2260
2261        // Check if branch is pushed to remote and push if needed
2262        debug!("Opening git repository to check branch status");
2263        let git_repo =
2264            crate::git::GitRepository::open().context("Failed to open git repository")?;
2265
2266        debug!(
2267            "Checking if branch '{}' exists on remote 'origin'",
2268            branch_name
2269        );
2270        if !git_repo.branch_exists_on_remote(branch_name, "origin")? {
2271            println!("๐Ÿ“ค Pushing branch to remote...");
2272            debug!(
2273                "Branch '{}' not found on remote, attempting to push",
2274                branch_name
2275            );
2276            git_repo
2277                .push_branch(branch_name, "origin")
2278                .context("Failed to push branch to remote")?;
2279        } else {
2280            debug!("Branch '{}' already exists on remote 'origin'", branch_name);
2281        }
2282
2283        // Create PR using gh CLI with explicit head branch
2284        debug!("Creating PR with gh CLI - title: '{}'", title);
2285        debug!("PR description length: {} characters", description.len());
2286        debug!("PR draft status: {}", is_draft);
2287
2288        let mut args = vec![
2289            "pr",
2290            "create",
2291            "--head",
2292            branch_name,
2293            "--title",
2294            title,
2295            "--body",
2296            description,
2297        ];
2298
2299        if is_draft {
2300            args.push("--draft");
2301        }
2302
2303        let pr_result = Command::new("gh")
2304            .args(&args)
2305            .output()
2306            .context("Failed to create pull request")?;
2307
2308        if pr_result.status.success() {
2309            let pr_url = String::from_utf8_lossy(&pr_result.stdout);
2310            let pr_url = pr_url.trim();
2311            debug!("PR created successfully with URL: {}", pr_url);
2312            println!("๐ŸŽ‰ Pull request created: {}", pr_url);
2313        } else {
2314            let error_msg = String::from_utf8_lossy(&pr_result.stderr);
2315            error!("gh CLI failed to create PR: {}", error_msg);
2316            anyhow::bail!("Failed to create pull request: {}", error_msg);
2317        }
2318
2319        Ok(())
2320    }
2321
2322    /// Update existing GitHub PR using gh CLI
2323    fn update_github_pr(
2324        &self,
2325        repo_view: &crate::data::RepositoryView,
2326        title: &str,
2327        description: &str,
2328    ) -> Result<()> {
2329        use std::process::Command;
2330
2331        // Get the first existing PR number (assuming we're updating the most recent one)
2332        let pr_number = repo_view
2333            .branch_prs
2334            .as_ref()
2335            .and_then(|prs| prs.first())
2336            .map(|pr| pr.number)
2337            .context("No existing PR found to update")?;
2338
2339        println!("๐Ÿš€ Updating pull request #{}...", pr_number);
2340        println!("   ๐Ÿ“‹ Title: {}", title);
2341
2342        debug!(
2343            pr_number = pr_number,
2344            title = %title,
2345            description_length = description.len(),
2346            description_preview = %description.lines().take(3).collect::<Vec<_>>().join("\\n"),
2347            "Updating GitHub PR with title and description"
2348        );
2349
2350        // Update PR using gh CLI
2351        let gh_args = [
2352            "pr",
2353            "edit",
2354            &pr_number.to_string(),
2355            "--title",
2356            title,
2357            "--body",
2358            description,
2359        ];
2360
2361        debug!(
2362            args = ?gh_args,
2363            "Executing gh command to update PR"
2364        );
2365
2366        let pr_result = Command::new("gh")
2367            .args(gh_args)
2368            .output()
2369            .context("Failed to update pull request")?;
2370
2371        if pr_result.status.success() {
2372            // Get the PR URL using the existing PR data
2373            if let Some(existing_pr) = repo_view.branch_prs.as_ref().and_then(|prs| prs.first()) {
2374                println!("๐ŸŽ‰ Pull request updated: {}", existing_pr.url);
2375            } else {
2376                println!("๐ŸŽ‰ Pull request #{} updated successfully!", pr_number);
2377            }
2378        } else {
2379            let error_msg = String::from_utf8_lossy(&pr_result.stderr);
2380            anyhow::bail!("Failed to update pull request: {}", error_msg);
2381        }
2382
2383        Ok(())
2384    }
2385
2386    /// Show model information from actual AI client
2387    fn show_model_info_from_client(
2388        &self,
2389        client: &crate::claude::client::ClaudeClient,
2390    ) -> Result<()> {
2391        use crate::claude::model_config::get_model_registry;
2392
2393        println!("๐Ÿค– AI Model Configuration:");
2394
2395        // Get actual metadata from the client
2396        let metadata = client.get_ai_client_metadata();
2397        let registry = get_model_registry();
2398
2399        if let Some(spec) = registry.get_model_spec(&metadata.model) {
2400            // Highlight the API identifier portion in yellow
2401            if metadata.model != spec.api_identifier {
2402                println!(
2403                    "   ๐Ÿ“ก Model: {} โ†’ \x1b[33m{}\x1b[0m",
2404                    metadata.model, spec.api_identifier
2405                );
2406            } else {
2407                println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", metadata.model);
2408            }
2409
2410            println!("   ๐Ÿท๏ธ  Provider: {}", spec.provider);
2411            println!("   ๐Ÿ“Š Generation: {}", spec.generation);
2412            println!("   โญ Tier: {} ({})", spec.tier, {
2413                if let Some(tier_info) = registry.get_tier_info(&spec.provider, &spec.tier) {
2414                    &tier_info.description
2415                } else {
2416                    "No description available"
2417                }
2418            });
2419            println!("   ๐Ÿ“ค Max output tokens: {}", spec.max_output_tokens);
2420            println!("   ๐Ÿ“ฅ Input context: {}", spec.input_context);
2421
2422            if spec.legacy {
2423                println!("   โš ๏ธ  Legacy model (consider upgrading to newer version)");
2424            }
2425        } else {
2426            // Fallback to client metadata if not in registry
2427            println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", metadata.model);
2428            println!("   ๐Ÿท๏ธ  Provider: {}", metadata.provider);
2429            println!("   โš ๏ธ  Model not found in registry, using client metadata:");
2430            println!("   ๐Ÿ“ค Max output tokens: {}", metadata.max_response_length);
2431            println!("   ๐Ÿ“ฅ Input context: {}", metadata.max_context_length);
2432        }
2433
2434        println!();
2435        Ok(())
2436    }
2437}