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