Skip to main content

git_iris/
cli.rs

1use crate::commands;
2use crate::common::CommonParams;
3use crate::log_debug;
4use crate::providers::Provider;
5use crate::theme;
6use crate::theme::names::tokens;
7use crate::ui;
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{CommandFactory, Parser, Subcommand, ValueEnum, crate_version};
10use clap_complete::{Shell, generate};
11use colored::Colorize;
12use std::io;
13
14/// Default log file path for debug output
15pub const LOG_FILE: &str = "git-iris-debug.log";
16
17/// CLI structure defining the available commands and global arguments
18#[derive(Parser)]
19#[command(
20    author,
21    version = crate_version!(),
22    about = "Git-Iris: AI-powered Git workflow assistant",
23    long_about = "Git-Iris enhances your Git workflow with AI-assisted commit messages, code reviews, changelogs, and more.",
24    disable_version_flag = true,
25    after_help = get_dynamic_help(),
26    styles = get_styles(),
27)]
28#[allow(clippy::struct_excessive_bools)]
29pub struct Cli {
30    /// Subcommands available for the CLI
31    #[command(subcommand)]
32    pub command: Option<Commands>,
33
34    /// Log debug messages to a file
35    #[arg(
36        short = 'l',
37        long = "log",
38        global = true,
39        help = "Log debug messages to a file"
40    )]
41    pub log: bool,
42
43    /// Specify a custom log file path
44    #[arg(
45        long = "log-file",
46        global = true,
47        help = "Specify a custom log file path"
48    )]
49    pub log_file: Option<String>,
50
51    /// Suppress non-essential output (spinners, waiting messages, etc.)
52    #[arg(
53        short = 'q',
54        long = "quiet",
55        global = true,
56        help = "Suppress non-essential output"
57    )]
58    pub quiet: bool,
59
60    /// Display the version
61    #[arg(
62        short = 'v',
63        long = "version",
64        global = true,
65        help = "Display the version"
66    )]
67    pub version: bool,
68
69    /// Repository URL to use instead of local repository
70    #[arg(
71        short = 'r',
72        long = "repo",
73        global = true,
74        help = "Repository URL to use instead of local repository"
75    )]
76    pub repository_url: Option<String>,
77
78    /// Enable debug mode for detailed agent observability
79    #[arg(
80        long = "debug",
81        global = true,
82        help = "Enable debug mode with gorgeous color-coded output showing agent execution details"
83    )]
84    pub debug: bool,
85
86    /// Override the theme for this session
87    #[arg(
88        long = "theme",
89        global = true,
90        help = "Override theme for this session (use 'git-iris themes' to list available)"
91    )]
92    pub theme: Option<String>,
93}
94
95/// Enumeration of available subcommands
96#[derive(Subcommand)]
97#[command(subcommand_negates_reqs = true)]
98#[command(subcommand_precedence_over_arg = true)]
99pub enum Commands {
100    // Feature commands first
101    /// Generate a commit message using AI
102    #[command(
103        about = "Generate a commit message using AI",
104        long_about = "Generate a commit message using AI based on the current Git context.",
105        after_help = get_dynamic_help(),
106        visible_alias = "commit"
107    )]
108    Gen {
109        #[command(flatten)]
110        common: CommonParams,
111
112        /// Automatically commit with the generated message
113        #[arg(short, long, help = "Automatically commit with the generated message")]
114        auto_commit: bool,
115
116        /// Print the generated message to stdout and exit
117        #[arg(short, long, help = "Print the generated message to stdout and exit")]
118        print: bool,
119
120        /// Skip the verification step (pre/post commit hooks)
121        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
122        no_verify: bool,
123
124        /// Amend the previous commit instead of creating a new one
125        #[arg(long, help = "Amend the previous commit with staged changes")]
126        amend: bool,
127    },
128
129    /// Review staged changes and provide feedback
130    #[command(
131        about = "Review staged changes using AI",
132        long_about = "Generate a comprehensive multi-dimensional code review of staged changes using AI. Analyzes code across 10 dimensions including complexity, security, performance, and more."
133    )]
134    Review {
135        #[command(flatten)]
136        common: CommonParams,
137
138        /// Print the generated review to stdout and exit
139        #[arg(short, long, help = "Print the generated review to stdout and exit")]
140        print: bool,
141
142        /// Output raw markdown without any console formatting
143        #[arg(long, help = "Output raw markdown without any console formatting")]
144        raw: bool,
145
146        /// Include unstaged changes in the review
147        #[arg(long, help = "Include unstaged changes in the review")]
148        include_unstaged: bool,
149
150        /// Review a specific commit by ID (hash, branch, or reference)
151        #[arg(
152            long,
153            help = "Review a specific commit by ID (hash, branch, or reference)"
154        )]
155        commit: Option<String>,
156
157        /// Starting branch for comparison (defaults to the repository's primary branch)
158        #[arg(
159            long,
160            help = "Starting branch for comparison (defaults to the repository's primary branch). Used with --to for explicit branch comparison reviews"
161        )]
162        from: Option<String>,
163
164        /// Target branch for comparison (e.g., 'feature-branch', 'pr-branch')
165        #[arg(
166            long,
167            help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews or on its own to compare from the repository's primary branch"
168        )]
169        to: Option<String>,
170
171        /// Publish the generated review as a GitHub PR review comment
172        #[arg(
173            long,
174            help = "Publish the generated review as a GitHub PR review comment"
175        )]
176        github_review: bool,
177
178        /// GitHub pull request number to publish to (auto-detects from branch when omitted)
179        #[arg(long = "pr", help = "GitHub pull request number to publish to")]
180        pull_number: Option<u64>,
181
182        /// Add inline comments for findings whose file and line references are present in the PR diff
183        #[arg(long, help = "Add validated inline comments for review findings")]
184        github_inline_comments: bool,
185
186        /// GitHub review event to submit
187        #[arg(
188            long = "github-review-event",
189            value_enum,
190            default_value_t = GitHubReviewEvent::Comment,
191            help = "GitHub review event to submit"
192        )]
193        github_review_event: GitHubReviewEvent,
194    },
195
196    /// Generate a pull request description
197    #[command(
198        about = "Generate a pull request description using AI",
199        long_about = "Generate a comprehensive pull request description based on commit ranges, branch differences, or single commits. Analyzes the overall changeset as an atomic unit and creates professional PR descriptions with summaries, detailed explanations, and testing notes.\n\nUsage examples:\n• Single commit: --from abc1234 or --to abc1234\n• Single commitish: --from HEAD~1 or --to HEAD~2\n• Multiple commits: --from HEAD~3 (reviews last 3 commits)\n• Commit range: --from abc1234 --to def5678\n• Branch comparison: --from <default-branch> --to feature-branch\n• From repository primary branch to target branch: --to feature-branch\n\nSupported commitish syntax: HEAD~2, HEAD^, @~3, <default-branch>~1, origin/<default-branch>^, etc."
200    )]
201    Pr {
202        #[command(flatten)]
203        common: CommonParams,
204
205        /// Print the generated PR description to stdout and exit
206        #[arg(
207            short,
208            long,
209            help = "Print the generated PR description to stdout and exit"
210        )]
211        print: bool,
212
213        /// Output raw markdown without any console formatting
214        #[arg(long, help = "Output raw markdown without any console formatting")]
215        raw: bool,
216
217        /// Copy raw markdown to clipboard
218        #[arg(
219            short,
220            long,
221            help = "Copy raw markdown to clipboard (for pasting into GitHub/GitLab)"
222        )]
223        copy: bool,
224
225        /// Starting branch, commit, or commitish for comparison
226        #[arg(
227            long,
228            help = "Starting branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash (e.g., --from abc1234). For reviewing multiple commits, use commitish syntax (e.g., --from HEAD~3 to review last 3 commits)"
229        )]
230        from: Option<String>,
231
232        /// Target branch, commit, or commitish for comparison
233        #[arg(
234            long,
235            help = "Target branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash or commitish (e.g., --to HEAD~2)"
236        )]
237        to: Option<String>,
238
239        /// Update the GitHub PR body with the generated description
240        #[arg(
241            long = "update",
242            visible_alias = "github-update",
243            help = "Update the GitHub PR body with the generated description"
244        )]
245        github_update: bool,
246
247        /// GitHub pull request number to update (auto-detects from branch when omitted)
248        #[arg(long = "pr", help = "GitHub pull request number to update")]
249        pull_number: Option<u64>,
250    },
251
252    /// Generate a changelog
253    #[command(
254        about = "Generate a changelog",
255        long_about = "Generate a changelog between two specified Git references."
256    )]
257    Changelog {
258        #[command(flatten)]
259        common: CommonParams,
260
261        /// Starting Git reference (commit hash, tag, or branch name)
262        #[arg(long, required = true)]
263        from: String,
264
265        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
266        #[arg(long)]
267        to: Option<String>,
268
269        /// Output raw markdown without any console formatting
270        #[arg(long, help = "Output raw markdown without any console formatting")]
271        raw: bool,
272
273        /// Update the changelog file with the new changes
274        #[arg(long, help = "Update the changelog file with the new changes")]
275        update: bool,
276
277        /// Path to the changelog file
278        #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
279        file: Option<String>,
280
281        /// Explicit version name to use in the changelog instead of getting it from Git
282        #[arg(long, help = "Explicit version name to use in the changelog")]
283        version_name: Option<String>,
284    },
285
286    /// Generate release notes
287    #[command(
288        about = "Generate release notes",
289        long_about = "Generate comprehensive release notes between two specified Git references."
290    )]
291    ReleaseNotes {
292        #[command(flatten)]
293        common: CommonParams,
294
295        /// Starting Git reference (commit hash, tag, or branch name)
296        #[arg(long, required = true)]
297        from: String,
298
299        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
300        #[arg(long)]
301        to: Option<String>,
302
303        /// Output raw markdown without any console formatting
304        #[arg(long, help = "Output raw markdown without any console formatting")]
305        raw: bool,
306
307        /// Update the release notes file with the new content
308        #[arg(long, help = "Update the release notes file with the new content")]
309        update: bool,
310
311        /// Path to the release notes file
312        #[arg(
313            long,
314            help = "Path to the release notes file (defaults to RELEASE_NOTES.md)"
315        )]
316        file: Option<String>,
317
318        /// Explicit version name to use in the release notes instead of getting it from Git
319        #[arg(long, help = "Explicit version name to use in the release notes")]
320        version_name: Option<String>,
321    },
322
323    /// Launch Iris Studio - unified TUI for all operations
324    #[command(
325        about = "Launch Iris Studio TUI",
326        long_about = "Launch Iris Studio, a unified terminal user interface for exploring code, generating commits, reviewing changes, and more. The interface adapts to your repository state."
327    )]
328    Studio {
329        #[command(flatten)]
330        common: CommonParams,
331
332        /// Initial mode to launch in
333        #[arg(
334            long,
335            value_name = "MODE",
336            help = "Initial mode: explore, commit, review, pr, changelog, release-notes"
337        )]
338        mode: Option<String>,
339
340        /// Starting ref for PR/changelog comparison (defaults to the repository's primary branch)
341        #[arg(long, value_name = "REF", help = "Starting ref for comparison")]
342        from: Option<String>,
343
344        /// Ending ref for PR/changelog comparison (defaults to HEAD)
345        #[arg(long, value_name = "REF", help = "Ending ref for comparison")]
346        to: Option<String>,
347    },
348
349    // Configuration and utility commands
350    /// Configure the AI-assisted Git commit message generator
351    #[command(about = "Configure Git-Iris settings and providers")]
352    Config {
353        #[command(flatten)]
354        common: CommonParams,
355
356        /// Set API key for the specified provider
357        #[arg(long, help = "Set API key for the specified provider")]
358        api_key: Option<String>,
359
360        /// Set fast model for the specified provider (used for status updates and simple tasks)
361        #[arg(
362            long,
363            help = "Set fast model for the specified provider (used for status updates and simple tasks)"
364        )]
365        fast_model: Option<String>,
366
367        /// Set token limit for the specified provider
368        #[arg(long, help = "Set token limit for the specified provider")]
369        token_limit: Option<usize>,
370
371        /// Set additional parameters for the specified provider
372        #[arg(
373            long,
374            help = "Set additional parameters for the specified provider (key=value)"
375        )]
376        param: Option<Vec<String>>,
377
378        /// Set timeout in seconds for parallel subagent tasks
379        #[arg(
380            long,
381            help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
382        )]
383        subagent_timeout: Option<u64>,
384    },
385
386    /// Create or update a project-specific configuration file
387    #[command(
388        about = "Manage project-specific configuration",
389        long_about = "Create or update a project-specific .irisconfig file in the repository root."
390    )]
391    ProjectConfig {
392        #[command(flatten)]
393        common: CommonParams,
394
395        /// Set fast model for the specified provider (used for status updates and simple tasks)
396        #[arg(
397            long,
398            help = "Set fast model for the specified provider (used for status updates and simple tasks)"
399        )]
400        fast_model: Option<String>,
401
402        /// Set token limit for the specified provider
403        #[arg(long, help = "Set token limit for the specified provider")]
404        token_limit: Option<usize>,
405
406        /// Set additional parameters for the specified provider
407        #[arg(
408            long,
409            help = "Set additional parameters for the specified provider (key=value)"
410        )]
411        param: Option<Vec<String>>,
412
413        /// Set timeout in seconds for parallel subagent tasks
414        #[arg(
415            long,
416            help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
417        )]
418        subagent_timeout: Option<u64>,
419
420        /// Print the current project configuration
421        #[arg(short, long, help = "Print the current project configuration")]
422        print: bool,
423    },
424
425    /// List available instruction presets
426    #[command(about = "List available instruction presets")]
427    ListPresets,
428
429    /// List available themes
430    #[command(about = "List available themes")]
431    Themes,
432
433    /// Generate shell completions
434    #[command(
435        about = "Generate shell completions",
436        long_about = "Generate shell completion scripts for bash, zsh, fish, elvish, or powershell.\n\nUsage examples:\n• Bash: git-iris completions bash >> ~/.bashrc\n• Zsh:  git-iris completions zsh >> ~/.zshrc\n• Fish: git-iris completions fish > ~/.config/fish/completions/git-iris.fish"
437    )]
438    Completions {
439        /// Shell to generate completions for
440        #[arg(value_enum)]
441        shell: Shell,
442    },
443
444    /// Manage Git hooks for automatic commit message generation
445    #[command(
446        about = "Install or uninstall the prepare-commit-msg Git hook",
447        long_about = "Install or uninstall a prepare-commit-msg Git hook that automatically generates commit messages using git-iris when you run 'git commit'."
448    )]
449    Hook {
450        /// Hook action to perform
451        #[command(subcommand)]
452        action: HookAction,
453    },
454}
455
456/// Hook management sub-commands
457#[derive(Subcommand)]
458pub enum HookAction {
459    /// Install the prepare-commit-msg hook
460    #[command(about = "Install the prepare-commit-msg hook")]
461    Install {
462        /// Overwrite an existing hook that wasn't installed by git-iris
463        #[arg(long, help = "Overwrite an existing hook not installed by git-iris")]
464        force: bool,
465    },
466    /// Uninstall the prepare-commit-msg hook
467    #[command(about = "Uninstall the prepare-commit-msg hook")]
468    Uninstall,
469}
470
471/// Define custom styles for Clap
472fn get_styles() -> Styles {
473    Styles::styled()
474        .header(AnsiColor::Magenta.on_default().bold())
475        .usage(AnsiColor::Cyan.on_default().bold())
476        .literal(AnsiColor::Green.on_default().bold())
477        .placeholder(AnsiColor::Yellow.on_default())
478        .valid(AnsiColor::Blue.on_default().bold())
479        .invalid(AnsiColor::Red.on_default().bold())
480        .error(AnsiColor::Red.on_default().bold())
481}
482
483/// Parse the command-line arguments
484#[must_use]
485pub fn parse_args() -> Cli {
486    Cli::parse()
487}
488
489/// Generate dynamic help including available LLM providers
490fn get_dynamic_help() -> String {
491    let providers_list = Provider::all_names()
492        .iter()
493        .map(|p| format!("{}", p.bold()))
494        .collect::<Vec<_>>()
495        .join(" • ");
496
497    format!("\nAvailable LLM Providers: {providers_list}")
498}
499
500/// Main function to parse arguments and handle the command
501///
502/// # Errors
503///
504/// Returns an error when command handling fails.
505pub async fn main() -> anyhow::Result<()> {
506    let cli = parse_args();
507
508    if cli.version {
509        ui::print_version(crate_version!());
510        return Ok(());
511    }
512
513    // Initialize logger with appropriate filter level — must happen before any log calls
514    if let Err(e) = crate::logger::init(cli.log) {
515        eprintln!("Warning: Failed to initialize logging: {e}");
516    }
517
518    if cli.log {
519        crate::logger::enable_logging();
520        crate::logger::set_log_to_stdout(true);
521        let log_file = cli.log_file.as_deref().unwrap_or(LOG_FILE);
522        crate::logger::set_log_file(log_file)?;
523        log_debug!("Debug logging enabled");
524    } else {
525        crate::logger::disable_logging();
526    }
527
528    // Set quiet mode in the UI module
529    if cli.quiet {
530        crate::ui::set_quiet_mode(true);
531    }
532
533    // Initialize theme
534    initialize_theme(cli.theme.as_deref());
535
536    // Enable debug mode if requested
537    if cli.debug {
538        crate::agents::debug::enable_debug_mode();
539        crate::agents::debug::debug_header("🔮 IRIS DEBUG MODE ACTIVATED 🔮");
540    }
541
542    if let Some(command) = cli.command {
543        handle_command(command, cli.repository_url).await
544    } else {
545        // Default: launch Studio with auto-detect mode
546        handle_studio(
547            CommonParams::default(),
548            None,
549            None,
550            None,
551            cli.repository_url,
552        )
553        .await
554    }
555}
556
557/// Initialize the theme from CLI flag or config
558fn initialize_theme(cli_theme: Option<&str>) {
559    use crate::config::Config;
560
561    // CLI flag takes precedence
562    let theme_name = if let Some(name) = cli_theme {
563        Some(name.to_string())
564    } else {
565        // Try to load from config
566        Config::load().ok().and_then(|c| {
567            if c.theme.is_empty() {
568                None
569            } else {
570                Some(c.theme)
571            }
572        })
573    };
574
575    // Load the theme if specified, otherwise default is already active
576    if let Some(name) = theme_name {
577        if let Err(e) = theme::load_theme_by_name(&name) {
578            ui::print_warning(&format!(
579                "Failed to load theme '{}': {}. Using default.",
580                name, e
581            ));
582        } else {
583            log_debug!("Loaded theme: {}", name);
584        }
585    }
586}
587
588/// Configuration for the Gen command
589#[allow(clippy::struct_excessive_bools)]
590struct GenConfig {
591    auto_commit: bool,
592    use_gitmoji: bool,
593    print_only: bool,
594    verify: bool,
595    amend: bool,
596}
597
598/// Handle the `Gen` command with agent framework and Studio integration
599#[allow(clippy::too_many_lines)]
600async fn handle_gen_with_agent(
601    common: CommonParams,
602    config: GenConfig,
603    repository_url: Option<String>,
604) -> anyhow::Result<()> {
605    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
606    use crate::config::Config;
607    use crate::git::GitRepo;
608    use crate::instruction_presets::PresetType;
609    use crate::output::format_commit_result;
610    use crate::services::GitCommitService;
611    use crate::studio::{Mode, run_studio};
612    use crate::types::format_commit_message;
613    use anyhow::Context;
614    use std::sync::Arc;
615
616    // Check if the preset is appropriate for commit messages
617    if !common.is_valid_preset_for_type(PresetType::Commit) {
618        ui::print_warning(
619            "The specified preset may not be suitable for commit messages. Consider using a commit or general preset instead.",
620        );
621        ui::print_info("Run 'git-iris list-presets' to see available presets for commits.");
622    }
623
624    // Amend mode requires --print or --auto-commit (Studio amend support coming later)
625    if config.amend && !config.print_only && !config.auto_commit {
626        ui::print_warning("--amend requires --print or --auto-commit for now.");
627        ui::print_info("Example: git-iris gen --amend --auto-commit");
628        return Ok(());
629    }
630
631    let mut cfg = Config::load()?;
632    common.apply_to_config(&mut cfg)?;
633
634    // Create git repo and services
635    let repo_url = repository_url.clone().or(common.repository_url.clone());
636    let git_repo = Arc::new(GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?);
637    let use_gitmoji = config.use_gitmoji && cfg.use_gitmoji;
638
639    // Create GitCommitService for commit operations
640    let commit_service = Arc::new(GitCommitService::new(
641        git_repo.clone(),
642        use_gitmoji,
643        config.verify,
644    ));
645
646    // Create IrisAgentService for LLM operations
647    let agent_service = Arc::new(IrisAgentService::from_common_params(
648        &common,
649        repository_url.clone(),
650    )?);
651
652    // Get git info for staged files check
653    let git_info = git_repo.get_git_info(&cfg)?;
654
655    // For --print or --auto-commit, we need to generate the message first
656    if config.print_only || config.auto_commit {
657        // For amend mode, we allow empty staged changes (amending message only)
658        // For regular commits, we require staged changes
659        if git_info.staged_files.is_empty() && !config.amend {
660            ui::print_warning(
661                "No staged changes. Please stage your changes before generating a commit message.",
662            );
663            ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
664            return Ok(());
665        }
666
667        // Run pre-commit hook before we do anything else
668        if let Err(e) = commit_service.pre_commit() {
669            ui::print_error(&format!("Pre-commit failed: {e}"));
670            return Err(e);
671        }
672
673        // Create spinner for agent mode
674        let spinner_msg = if config.amend {
675            "Generating amended commit message..."
676        } else {
677            "Generating commit message..."
678        };
679        let spinner = ui::create_spinner(spinner_msg);
680
681        // Use IrisAgentService for commit message generation
682        // For amend, we pass the original message as context
683        let context = if config.amend {
684            let original_message = commit_service.get_head_commit_message().unwrap_or_default();
685            TaskContext::for_amend(original_message)
686        } else {
687            TaskContext::for_gen()
688        };
689        let response = agent_service.execute_task("commit", context).await?;
690
691        // Extract commit message from response
692        let StructuredResponse::CommitMessage(generated_message) = response else {
693            return Err(anyhow::anyhow!("Expected commit message response"));
694        };
695
696        // Finish spinner after agent completes
697        spinner.finish_and_clear();
698
699        if config.print_only {
700            println!("{}", format_commit_message(&generated_message));
701            return Ok(());
702        }
703
704        // Auto-commit/amend mode
705        if commit_service.is_remote() {
706            ui::print_error(
707                "Cannot automatically commit to a remote repository. Use --print instead.",
708            );
709            return Err(anyhow::anyhow!(
710                "Auto-commit not supported for remote repositories"
711            ));
712        }
713
714        let commit_result = if config.amend {
715            commit_service.perform_amend(&format_commit_message(&generated_message))
716        } else {
717            commit_service.perform_commit(&format_commit_message(&generated_message))
718        };
719
720        match commit_result {
721            Ok(result) => {
722                let output =
723                    format_commit_result(&result, &format_commit_message(&generated_message));
724                println!("{output}");
725            }
726            Err(e) => {
727                let action = if config.amend { "amend" } else { "commit" };
728                eprintln!("Failed to {action}: {e}");
729                return Err(e);
730            }
731        }
732        return Ok(());
733    }
734
735    // Interactive mode: launch Studio (it handles staged check and auto-generation)
736    if commit_service.is_remote() {
737        ui::print_warning(
738            "Interactive commit not available for remote repositories. Use --print instead.",
739        );
740        return Ok(());
741    }
742
743    // Launch Studio in Commit mode - it will auto-generate if there are staged changes
744    run_studio(
745        cfg,
746        Some(git_repo),
747        Some(commit_service),
748        Some(agent_service),
749        Some(Mode::Commit),
750        None,
751        None,
752    )
753}
754
755/// Handle the `Gen` command
756async fn handle_gen(
757    common: CommonParams,
758    config: GenConfig,
759    repository_url: Option<String>,
760) -> anyhow::Result<()> {
761    log_debug!(
762        "Handling 'gen' command with common: {:?}, auto_commit: {}, use_gitmoji: {}, print: {}, verify: {}, amend: {}",
763        common,
764        config.auto_commit,
765        config.use_gitmoji,
766        config.print_only,
767        config.verify,
768        config.amend
769    );
770
771    ui::print_version(crate_version!());
772    ui::print_newline();
773
774    handle_gen_with_agent(common, config, repository_url).await
775}
776
777/// Handle the `Config` command
778fn handle_config(
779    common: &CommonParams,
780    api_key: Option<String>,
781    model: Option<String>,
782    fast_model: Option<String>,
783    token_limit: Option<usize>,
784    param: Option<Vec<String>>,
785    subagent_timeout: Option<u64>,
786) -> anyhow::Result<()> {
787    log_debug!(
788        "Handling 'config' command with common: {:?}, api_key: {}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
789        common,
790        if api_key.is_some() {
791            "[REDACTED]"
792        } else {
793            "<none>"
794        },
795        model,
796        token_limit,
797        param,
798        subagent_timeout
799    );
800    commands::handle_config_command(
801        common,
802        api_key,
803        model,
804        fast_model,
805        token_limit,
806        param,
807        subagent_timeout,
808    )
809}
810
811/// Handle the `Review` command
812struct ReviewOutputOptions {
813    mode: OutputMode,
814    github_review: bool,
815    github_inline_comments: bool,
816    github_review_event: GitHubReviewEvent,
817}
818
819#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
820pub enum GitHubReviewEvent {
821    Comment,
822    RequestChanges,
823    Approve,
824}
825
826impl From<GitHubReviewEvent> for octocrab::models::pulls::ReviewAction {
827    fn from(event: GitHubReviewEvent) -> Self {
828        match event {
829            GitHubReviewEvent::Comment => Self::Comment,
830            GitHubReviewEvent::RequestChanges => Self::RequestChanges,
831            GitHubReviewEvent::Approve => Self::Approve,
832        }
833    }
834}
835
836#[derive(Debug, Clone, Copy, PartialEq, Eq)]
837enum OutputMode {
838    Default,
839    Print,
840    Raw,
841}
842
843impl OutputMode {
844    const fn from_flags(print: bool, raw: bool) -> Self {
845        if raw {
846            Self::Raw
847        } else if print {
848            Self::Print
849        } else {
850            Self::Default
851        }
852    }
853}
854
855async fn handle_review(
856    common: CommonParams,
857    output: ReviewOutputOptions,
858    repository_url: Option<String>,
859    include_unstaged: bool,
860    commit: Option<String>,
861    from: Option<String>,
862    to: Option<String>,
863    pull_number: Option<u64>,
864) -> anyhow::Result<()> {
865    log_debug!(
866        "Handling 'review' command with common: {:?}, print: {}, raw: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
867        common,
868        output.mode == OutputMode::Print,
869        output.mode == OutputMode::Raw,
870        include_unstaged,
871        commit,
872        from,
873        to
874    );
875
876    // For raw output, skip all formatting
877    if output.mode != OutputMode::Raw {
878        ui::print_version(crate_version!());
879        ui::print_newline();
880    }
881
882    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
883    use crate::github::{GitHubClient, ReviewPublishOptions};
884
885    // Create spinner for progress indication (skip for raw output)
886    let spinner = if output.mode == OutputMode::Raw {
887        None
888    } else {
889        Some(ui::create_spinner("Initializing Iris..."))
890    };
891
892    // Use IrisAgentService for agent execution
893    let service = IrisAgentService::from_common_params(&common, repository_url)?;
894    let default_base = service
895        .git_repo()
896        .and_then(|repo| repo.get_default_base_ref().ok())
897        .unwrap_or_else(|| "main".to_string());
898    let context =
899        TaskContext::for_review_with_base(commit, from, to, include_unstaged, &default_base)?;
900    let response = service.execute_task("review", context).await?;
901
902    // Finish spinner
903    if let Some(s) = spinner {
904        s.finish_and_clear();
905    }
906
907    let review_content = match &response {
908        StructuredResponse::MarkdownReview(review) => review.content.clone(),
909        _ => response.to_string(),
910    };
911
912    if output.github_review {
913        let git_repo = service
914            .git_repo()
915            .ok_or_else(|| anyhow::anyhow!("GitHub publishing requires a git repository"))?;
916        let github = GitHubClient::from_git_repo(git_repo)?;
917        let number = github.resolve_pull_number(pull_number, git_repo).await?;
918        github
919            .publish_review(
920                number,
921                &review_content,
922                ReviewPublishOptions {
923                    event: output.github_review_event.into(),
924                    inline_comments: output.github_inline_comments,
925                },
926            )
927            .await?;
928        if output.mode != OutputMode::Raw {
929            ui::print_success(&format!(
930                "Published review to {}/{} PR #{}",
931                github.repo().owner,
932                github.repo().name,
933                number
934            ));
935        }
936    }
937
938    if output.mode == OutputMode::Raw {
939        println!("{review_content}");
940    } else if output.mode == OutputMode::Print {
941        println!("{response}");
942    } else {
943        ui::print_success("Code review completed successfully");
944        println!("{response}");
945    }
946    Ok(())
947}
948
949/// Handle the `Changelog` command
950#[allow(clippy::too_many_arguments)]
951async fn handle_changelog(
952    common: CommonParams,
953    from: String,
954    to: Option<String>,
955    raw: bool,
956    repository_url: Option<String>,
957    update: bool,
958    file: Option<String>,
959    version_name: Option<String>,
960) -> anyhow::Result<()> {
961    log_debug!(
962        "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
963        common,
964        from,
965        to,
966        raw,
967        update,
968        file,
969        version_name
970    );
971
972    // For raw output, skip all formatting
973    if !raw {
974        ui::print_version(crate_version!());
975        ui::print_newline();
976    }
977
978    use crate::agents::{IrisAgentService, TaskContext};
979    use crate::changelog::ChangelogGenerator;
980    use crate::git::GitRepo;
981    use anyhow::Context;
982    use std::sync::Arc;
983
984    // Create structured context for changelog with version_name and current date
985    let context = TaskContext::for_changelog(from.clone(), to.clone(), version_name.clone(), None);
986    let to_ref = to.unwrap_or_else(|| "HEAD".to_string());
987
988    // Create spinner for progress indication (skip for raw output)
989    let spinner = if raw {
990        None
991    } else {
992        Some(ui::create_spinner("Initializing Iris..."))
993    };
994
995    // Use IrisAgentService for agent execution
996    let service = IrisAgentService::from_common_params(&common, repository_url.clone())?;
997    let response = service.execute_task("changelog", context).await?;
998
999    // Finish spinner
1000    if let Some(s) = spinner {
1001        s.finish_and_clear();
1002    }
1003
1004    // Print the changelog
1005    println!("{response}");
1006
1007    if update {
1008        // Extract the formatted content for file update
1009        let formatted_content = response.to_string();
1010        let changelog_path = file.unwrap_or_else(|| "CHANGELOG.md".to_string());
1011        let repo_url_for_update = repository_url.or(common.repository_url.clone());
1012
1013        // Create GitRepo for file update
1014        let git_repo = if let Some(url) = repo_url_for_update {
1015            Arc::new(
1016                GitRepo::clone_remote_repository(&url)
1017                    .context("Failed to clone repository for changelog update")?,
1018            )
1019        } else {
1020            let repo_path = std::env::current_dir()?;
1021            Arc::new(
1022                GitRepo::new(&repo_path)
1023                    .context("Failed to create GitRepo for changelog update")?,
1024            )
1025        };
1026
1027        // Update changelog file
1028        let update_spinner =
1029            ui::create_spinner(&format!("Updating changelog file at {changelog_path}..."));
1030
1031        match ChangelogGenerator::update_changelog_file(
1032            &formatted_content,
1033            &changelog_path,
1034            &git_repo,
1035            &to_ref,
1036            version_name,
1037        ) {
1038            Ok(()) => {
1039                update_spinner.finish_and_clear();
1040                ui::print_success(&format!(
1041                    "✨ Changelog successfully updated at {}",
1042                    changelog_path.bright_green()
1043                ));
1044            }
1045            Err(e) => {
1046                update_spinner.finish_and_clear();
1047                ui::print_error(&format!("Failed to update changelog file: {e}"));
1048                return Err(e);
1049            }
1050        }
1051    }
1052    Ok(())
1053}
1054
1055/// Handle the `Release Notes` command
1056#[allow(clippy::too_many_arguments)]
1057async fn handle_release_notes(
1058    common: CommonParams,
1059    from: String,
1060    to: Option<String>,
1061    raw: bool,
1062    repository_url: Option<String>,
1063    update: bool,
1064    file: Option<String>,
1065    version_name: Option<String>,
1066) -> anyhow::Result<()> {
1067    log_debug!(
1068        "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
1069        common,
1070        from,
1071        to,
1072        raw,
1073        update,
1074        file,
1075        version_name
1076    );
1077
1078    // For raw output, skip all formatting
1079    if !raw {
1080        ui::print_version(crate_version!());
1081        ui::print_newline();
1082    }
1083
1084    use crate::agents::{IrisAgentService, TaskContext};
1085    use std::fs;
1086    use std::path::Path;
1087
1088    // Create structured context for release notes with version_name and current date
1089    let context = TaskContext::for_changelog(from, to, version_name, None);
1090
1091    // Create spinner for progress indication (skip for raw output)
1092    let spinner = if raw {
1093        None
1094    } else {
1095        Some(ui::create_spinner("Initializing Iris..."))
1096    };
1097
1098    // Use IrisAgentService for agent execution
1099    let service = IrisAgentService::from_common_params(&common, repository_url)?;
1100    let response = service.execute_task("release_notes", context).await?;
1101
1102    // Finish spinner
1103    if let Some(s) = spinner {
1104        s.finish_and_clear();
1105    }
1106
1107    println!("{response}");
1108
1109    // Handle --update flag
1110    if update {
1111        let release_notes_path = file.unwrap_or_else(|| "RELEASE_NOTES.md".to_string());
1112        let formatted_content = response.to_string();
1113
1114        let update_spinner = ui::create_spinner(&format!(
1115            "Updating release notes file at {release_notes_path}..."
1116        ));
1117
1118        // Write or append to file
1119        let path = Path::new(&release_notes_path);
1120        let result = if path.exists() {
1121            // Prepend to existing file
1122            let existing = fs::read_to_string(path)?;
1123            fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
1124        } else {
1125            // Create new file
1126            fs::write(path, &formatted_content)
1127        };
1128
1129        match result {
1130            Ok(()) => {
1131                update_spinner.finish_and_clear();
1132                ui::print_success(&format!(
1133                    "✨ Release notes successfully updated at {}",
1134                    release_notes_path.bright_green()
1135                ));
1136            }
1137            Err(e) => {
1138                update_spinner.finish_and_clear();
1139                ui::print_error(&format!("Failed to update release notes file: {e}"));
1140                return Err(e.into());
1141            }
1142        }
1143    }
1144
1145    Ok(())
1146}
1147
1148/// Handle the command based on parsed arguments
1149#[allow(clippy::too_many_lines)]
1150///
1151/// # Errors
1152///
1153/// Returns an error when the selected command fails.
1154pub async fn handle_command(
1155    command: Commands,
1156    repository_url: Option<String>,
1157) -> anyhow::Result<()> {
1158    match command {
1159        Commands::Gen {
1160            common,
1161            auto_commit,
1162            print,
1163            no_verify,
1164            amend,
1165        } => {
1166            // Get gitmoji setting from common params (--gitmoji/--no-gitmoji flags)
1167            // Default to true if not explicitly set
1168            let use_gitmoji = common.resolved_gitmoji().unwrap_or(true);
1169            handle_gen(
1170                common,
1171                GenConfig {
1172                    auto_commit,
1173                    use_gitmoji,
1174                    print_only: print,
1175                    verify: !no_verify,
1176                    amend,
1177                },
1178                repository_url,
1179            )
1180            .await
1181        }
1182        Commands::Config {
1183            common,
1184            api_key,
1185            fast_model,
1186            token_limit,
1187            param,
1188            subagent_timeout,
1189        } => handle_config(
1190            &common,
1191            api_key,
1192            common.model.clone(),
1193            fast_model,
1194            token_limit,
1195            param,
1196            subagent_timeout,
1197        ),
1198        Commands::Review {
1199            common,
1200            print,
1201            raw,
1202            include_unstaged,
1203            commit,
1204            from,
1205            to,
1206            github_review,
1207            github_inline_comments,
1208            github_review_event,
1209            pull_number,
1210        } => {
1211            handle_review(
1212                common,
1213                ReviewOutputOptions {
1214                    mode: OutputMode::from_flags(print, raw),
1215                    github_review,
1216                    github_inline_comments,
1217                    github_review_event,
1218                },
1219                repository_url,
1220                include_unstaged,
1221                commit,
1222                from,
1223                to,
1224                pull_number,
1225            )
1226            .await
1227        }
1228        Commands::Changelog {
1229            common,
1230            from,
1231            to,
1232            raw,
1233            update,
1234            file,
1235            version_name,
1236        } => {
1237            handle_changelog(
1238                common,
1239                from,
1240                to,
1241                raw,
1242                repository_url,
1243                update,
1244                file,
1245                version_name,
1246            )
1247            .await
1248        }
1249        Commands::ReleaseNotes {
1250            common,
1251            from,
1252            to,
1253            raw,
1254            update,
1255            file,
1256            version_name,
1257        } => {
1258            handle_release_notes(
1259                common,
1260                from,
1261                to,
1262                raw,
1263                repository_url,
1264                update,
1265                file,
1266                version_name,
1267            )
1268            .await
1269        }
1270        Commands::ProjectConfig {
1271            common,
1272            fast_model,
1273            token_limit,
1274            param,
1275            subagent_timeout,
1276            print,
1277        } => commands::handle_project_config_command(
1278            &common,
1279            common.model.clone(),
1280            fast_model,
1281            token_limit,
1282            param,
1283            subagent_timeout,
1284            print,
1285        ),
1286        Commands::ListPresets => commands::handle_list_presets_command(),
1287        Commands::Themes => {
1288            handle_themes();
1289            Ok(())
1290        }
1291        Commands::Completions { shell } => {
1292            handle_completions(shell);
1293            Ok(())
1294        }
1295        Commands::Hook { action } => commands::handle_hook_command(&action),
1296        Commands::Pr {
1297            common,
1298            print,
1299            raw,
1300            copy,
1301            from,
1302            to,
1303            github_update,
1304            pull_number,
1305        } => {
1306            handle_pr(
1307                common,
1308                PrOutputOptions {
1309                    mode: OutputMode::from_flags(print, raw),
1310                    copy,
1311                    github_update,
1312                },
1313                from,
1314                to,
1315                pull_number,
1316                repository_url,
1317            )
1318            .await
1319        }
1320        Commands::Studio {
1321            common,
1322            mode,
1323            from,
1324            to,
1325        } => handle_studio(common, mode, from, to, repository_url).await,
1326    }
1327}
1328
1329/// Handle the `Themes` command - list available themes
1330fn handle_themes() {
1331    ui::print_version(crate_version!());
1332    ui::print_newline();
1333
1334    let available = theme::list_available_themes();
1335    let current = theme::current();
1336    let current_name = &current.meta.name;
1337
1338    // Header
1339    let header_color = theme::current().color(tokens::ACCENT_PRIMARY);
1340    println!(
1341        "{}",
1342        "Available Themes:"
1343            .truecolor(header_color.r, header_color.g, header_color.b)
1344            .bold()
1345    );
1346    println!();
1347
1348    for info in available {
1349        let is_current = info.display_name == *current_name;
1350        let marker = if is_current { "● " } else { "  " };
1351
1352        let name_color = if is_current {
1353            theme::current().color(tokens::SUCCESS)
1354        } else {
1355            theme::current().color(tokens::ACCENT_SECONDARY)
1356        };
1357
1358        let desc_color = theme::current().color(tokens::TEXT_SECONDARY);
1359
1360        print!(
1361            "{}{}",
1362            marker.truecolor(name_color.r, name_color.g, name_color.b),
1363            info.name
1364                .truecolor(name_color.r, name_color.g, name_color.b)
1365                .bold()
1366        );
1367
1368        // Show display name if different from filename
1369        if info.display_name != info.name {
1370            print!(
1371                " ({})",
1372                info.display_name
1373                    .truecolor(desc_color.r, desc_color.g, desc_color.b)
1374            );
1375        }
1376
1377        // Show variant
1378        let variant_str = match info.variant {
1379            theme::ThemeVariant::Dark => "dark",
1380            theme::ThemeVariant::Light => "light",
1381        };
1382        let dim_color = theme::current().color(tokens::TEXT_DIM);
1383        print!(
1384            " [{}]",
1385            variant_str.truecolor(dim_color.r, dim_color.g, dim_color.b)
1386        );
1387
1388        if is_current {
1389            let active_color = theme::current().color(tokens::SUCCESS);
1390            print!(
1391                " {}",
1392                "(active)".truecolor(active_color.r, active_color.g, active_color.b)
1393            );
1394        }
1395
1396        println!();
1397    }
1398
1399    println!();
1400
1401    // Usage hint
1402    let hint_color = theme::current().color(tokens::TEXT_DIM);
1403    println!(
1404        "{}",
1405        "Use --theme <name> to override, or set 'theme' in config.toml".truecolor(
1406            hint_color.r,
1407            hint_color.g,
1408            hint_color.b
1409        )
1410    );
1411}
1412
1413/// Handle the `Completions` command - generate shell completion scripts
1414fn handle_completions(shell: Shell) {
1415    let mut cmd = Cli::command();
1416    generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1417}
1418
1419/// Handle the `Pr` command with agent framework
1420struct PrOutputOptions {
1421    mode: OutputMode,
1422    copy: bool,
1423    github_update: bool,
1424}
1425
1426struct GitHubUpdateContext {
1427    github: crate::github::GitHubClient,
1428    number: u64,
1429    existing_body: String,
1430}
1431
1432async fn github_update_context(
1433    service: &crate::agents::IrisAgentService,
1434    pull_number: Option<u64>,
1435) -> anyhow::Result<GitHubUpdateContext> {
1436    let git_repo = service
1437        .git_repo()
1438        .ok_or_else(|| anyhow::anyhow!("GitHub publishing requires a git repository"))?;
1439    let github = crate::github::GitHubClient::from_git_repo(git_repo)?;
1440    let number = github.resolve_pull_number(pull_number, git_repo).await?;
1441    let existing_body = github.pull_body(number).await?;
1442
1443    Ok(GitHubUpdateContext {
1444        github,
1445        number,
1446        existing_body,
1447    })
1448}
1449
1450fn pull_request_template_context(
1451    service: &crate::agents::IrisAgentService,
1452) -> anyhow::Result<Option<crate::agents::context::PullRequestTemplateContext>> {
1453    let Some(repo) = service.git_repo() else {
1454        return Ok(None);
1455    };
1456
1457    Ok(
1458        crate::github::find_pull_request_template(repo.repo_path())?.map(|template| {
1459            crate::agents::context::PullRequestTemplateContext {
1460                path: template.path,
1461                body: template.body,
1462            }
1463        }),
1464    )
1465}
1466
1467async fn handle_pr_with_agent(
1468    common: CommonParams,
1469    output: PrOutputOptions,
1470    from: Option<String>,
1471    to: Option<String>,
1472    pull_number: Option<u64>,
1473    repository_url: Option<String>,
1474) -> anyhow::Result<()> {
1475    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
1476    use crate::instruction_presets::PresetType;
1477    use arboard::Clipboard;
1478
1479    // Check if the preset is appropriate for PR descriptions (skip for raw output only)
1480    if output.mode != OutputMode::Raw
1481        && !common.is_valid_preset_for_type(PresetType::Review)
1482        && !common.is_valid_preset_for_type(PresetType::Both)
1483    {
1484        ui::print_warning(
1485            "The specified preset may not be suitable for PR descriptions. Consider using a review or general preset instead.",
1486        );
1487        ui::print_info("Run 'git-iris list-presets' to see available presets for PRs.");
1488    }
1489
1490    // Create spinner for progress indication (skip for raw output only)
1491    let spinner = if output.mode == OutputMode::Raw {
1492        None
1493    } else {
1494        Some(ui::create_spinner("Initializing Iris..."))
1495    };
1496
1497    // Use IrisAgentService for agent execution
1498    let service = IrisAgentService::from_common_params(&common, repository_url)?;
1499    let default_base = service
1500        .git_repo()
1501        .and_then(|repo| repo.get_default_base_ref().ok())
1502        .unwrap_or_else(|| "main".to_string());
1503    let github_context = if output.github_update {
1504        Some(github_update_context(&service, pull_number).await?)
1505    } else {
1506        None
1507    };
1508    let existing_body = github_context.as_ref().and_then(|context| {
1509        (!context.existing_body.trim().is_empty()).then(|| context.existing_body.clone())
1510    });
1511    let template = pull_request_template_context(&service)?;
1512    let context =
1513        TaskContext::for_pr_update_with_base(from, to, &default_base, existing_body, template);
1514    let response = service.execute_task("pr", context).await?;
1515
1516    // Finish spinner
1517    if let Some(s) = spinner {
1518        s.finish_and_clear();
1519    }
1520
1521    // Extract PR from response
1522    let StructuredResponse::PullRequest(generated_pr) = response else {
1523        return Err(anyhow::anyhow!("Expected pull request response"));
1524    };
1525    let raw_content = generated_pr.raw_content();
1526
1527    if output.github_update {
1528        let github_context = github_context.expect("GitHub update context should be loaded");
1529        github_context
1530            .github
1531            .update_pull_body(github_context.number, raw_content)
1532            .await?;
1533        if output.mode != OutputMode::Raw {
1534            ui::print_success(&format!(
1535                "Updated {}/{} PR #{}",
1536                github_context.github.repo().owner,
1537                github_context.github.repo().name,
1538                github_context.number
1539            ));
1540        }
1541    }
1542
1543    // Handle clipboard copy
1544    if output.copy {
1545        match Clipboard::new() {
1546            Ok(mut clipboard) => match clipboard.set_text(raw_content) {
1547                Ok(()) => {
1548                    ui::print_success("PR description copied to clipboard");
1549                }
1550                Err(e) => {
1551                    ui::print_error(&format!("Failed to copy to clipboard: {e}"));
1552                    // Fall back to printing raw
1553                    println!("{raw_content}");
1554                }
1555            },
1556            Err(e) => {
1557                ui::print_error(&format!("Clipboard unavailable: {e}"));
1558                // Fall back to printing raw
1559                println!("{raw_content}");
1560            }
1561        }
1562    } else if output.mode == OutputMode::Raw {
1563        // Raw markdown for piping to files or APIs
1564        println!("{}", generated_pr.raw_content());
1565    } else if output.mode == OutputMode::Print {
1566        // Formatted output for terminal viewing
1567        println!("{}", generated_pr.format());
1568    } else {
1569        ui::print_success("PR description generated successfully");
1570        println!("{}", generated_pr.format());
1571    }
1572
1573    Ok(())
1574}
1575
1576/// Handle the `Pr` command
1577async fn handle_pr(
1578    common: CommonParams,
1579    output: PrOutputOptions,
1580    from: Option<String>,
1581    to: Option<String>,
1582    pull_number: Option<u64>,
1583    repository_url: Option<String>,
1584) -> anyhow::Result<()> {
1585    log_debug!(
1586        "Handling 'pr' command with common: {:?}, print: {}, raw: {}, copy: {}, from: {:?}, to: {:?}",
1587        common,
1588        output.mode == OutputMode::Print,
1589        output.mode == OutputMode::Raw,
1590        output.copy,
1591        from,
1592        to
1593    );
1594
1595    // For raw output, skip version banner (piped output should be clean)
1596    // For copy mode, show the banner since we're giving user feedback
1597    if output.mode != OutputMode::Raw {
1598        ui::print_version(crate_version!());
1599        ui::print_newline();
1600    }
1601
1602    handle_pr_with_agent(common, output, from, to, pull_number, repository_url).await
1603}
1604
1605/// Handle the `Studio` command
1606#[allow(clippy::unused_async)] // Will need async when agent integration is complete
1607async fn handle_studio(
1608    common: CommonParams,
1609    mode: Option<String>,
1610    from: Option<String>,
1611    to: Option<String>,
1612    repository_url: Option<String>,
1613) -> anyhow::Result<()> {
1614    use crate::agents::IrisAgentService;
1615    use crate::config::Config;
1616    use crate::git::GitRepo;
1617    use crate::services::GitCommitService;
1618    use crate::studio::{Mode, run_studio};
1619    use anyhow::Context;
1620    use std::sync::Arc;
1621
1622    // Disable stdout logging immediately for TUI mode - it owns the terminal
1623    crate::logger::set_log_to_stdout(false);
1624
1625    log_debug!(
1626        "Handling 'studio' command with common: {:?}, mode: {:?}, from: {:?}, to: {:?}",
1627        common,
1628        mode,
1629        from,
1630        to
1631    );
1632
1633    let mut cfg = Config::load()?;
1634    common.apply_to_config(&mut cfg)?;
1635
1636    // Create git repo
1637    let repo_url = repository_url.clone().or(common.repository_url.clone());
1638    let git_repo =
1639        Arc::new(GitRepo::new_from_url(repo_url.clone()).context("Failed to create GitRepo")?);
1640
1641    // Create services
1642    let commit_service = Arc::new(GitCommitService::new(
1643        git_repo.clone(),
1644        cfg.use_gitmoji,
1645        true, // verify hooks
1646    ));
1647
1648    let agent_service = Arc::new(IrisAgentService::from_common_params(
1649        &common,
1650        repository_url,
1651    )?);
1652
1653    // Parse initial mode
1654    let initial_mode = mode
1655        .as_deref()
1656        .and_then(|m| match m.to_lowercase().as_str() {
1657            "explore" => Some(Mode::Explore),
1658            "commit" => Some(Mode::Commit),
1659            "review" => Some(Mode::Review),
1660            "pr" => Some(Mode::PR),
1661            "changelog" => Some(Mode::Changelog),
1662            "release-notes" | "release_notes" => Some(Mode::ReleaseNotes),
1663            _ => {
1664                ui::print_warning(&format!("Unknown mode '{}', using auto-detect", m));
1665                None
1666            }
1667        });
1668
1669    run_studio(
1670        cfg,
1671        Some(git_repo),
1672        Some(commit_service),
1673        Some(agent_service),
1674        initial_mode,
1675        from,
1676        to,
1677    )
1678}