//! The Source Control page, after T3 Code's: the version control and //! hosting tools found on this machine, then how worktree branches are //! named. use std::process::{Command, Stdio}; use gpui_kit::component::{Icon, input::Textarea}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use super::SettingsView; use super::widgets::{Section, SettingRow, choice, code, empty_row, reset, text_field}; use crate::settings::{self, BranchNaming}; use crate::ui::icons::Lucide; use crate::ui::metal::{self, Led, MetalButton, MetalStyled as _}; use crate::ui::workspace; const BRANCH_NAMING: &[(BranchNaming, &str)] = &[ (BranchNaming::Static, "Static prefix"), (BranchNaming::Semantic, "Semantic prefix"), (BranchNaming::Custom, "Custom instructions"), ]; /// One tool the page found, or didn't. #[derive(Clone, Debug)] pub(super) struct Tool { pub version: Option, /// "Available", "Authenticated as octocat", or why it isn't. pub summary: String, pub ok: bool, } /// What the last scan found. #[derive(Default)] pub(super) struct SourceControlState { scanning: bool, git: Option, github: Option, } /// Runs a program and returns its trimmed output, if it succeeded. fn output(program: &std::path::Path, args: &[&str]) -> Option { let output = Command::new(program).args(args).stdin(Stdio::null()).output().ok()?; output.status.success().then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) } fn scan_git() -> Tool { let Some(git) = workspace::find_program(&["git"]) else { return Tool { version: None, summary: "Not available on this machine.".into(), ok: false }; }; let version = output(&git, &["--version"]).map(|text| text.trim_start_matches("git version ").to_string()); Tool { version, summary: "Available".into(), ok: true } } fn scan_github() -> Tool { let Some(gh) = workspace::find_program(&["gh"]) else { return Tool { version: None, summary: "Install the GitHub CLI (gh) to check pull requests for auto-settle.".into(), ok: false, }; }; let version = output(&gh, &["--version"]) .and_then(|text| text.lines().next().map(str::to_string)) .and_then(|line| line.split_whitespace().nth(2).map(str::to_string)); match output(&gh, &["api", "user", "--jq", ".login"]) { Some(login) if !login.is_empty() => Tool { version, summary: format!("Authenticated as {login}"), ok: true }, _ => Tool { version, summary: "Not authenticated. Run gh auth login.".into(), ok: false }, } } impl SettingsView { /// Looks for git and the GitHub CLI in the background. pub(super) fn scan_source_control(&mut self, cx: &mut Context) { if self.source_control.scanning { return; } self.source_control.scanning = true; let task = cx.background_executor().spawn(async { (scan_git(), scan_github()) }); cx.spawn(async move |this, cx| { let (git, github) = task.await; let _ = this.update(cx, |this, cx| { this.source_control = SourceControlState { scanning: false, git: Some(git), github: Some(github) }; cx.notify(); }); }) .detach(); cx.notify(); } pub(super) fn render_source_control(&mut self, window: &mut Window, cx: &mut Context) -> Vec { let p = metal::palette(cx); let saved = settings::load(); let view = cx.weak_entity(); if self.source_control.git.is_none() && !self.source_control.scanning { self.scan_source_control(cx); } let scanning = self.source_control.scanning; let tool_row = |name: &'static str, icon: Lucide, tool: Option<&Tool>| -> AnyElement { let Some(tool) = tool else { return empty_row(format!("Looking for {name}…"), p); }; SettingRow::new(name) .icon(Icon::new(icon).size(px(14.)).text_color(p.ink2)) .when_some(tool.version.clone(), |row, version| row.title_suffix(code(version, p))) .description(tool.summary.clone()) .control(metal::led(SharedString::from(format!("tool-{name}")), if tool.ok { Led::Live } else { Led::Off }, false, p)) .into_any_element() }; let rescan = MetalButton::new("rescan-source-control") .ghost() .mini() .icon(gpui_kit::component::IconName::RefreshCw) .tooltip("Rescan Git and hosting integrations") .disabled(scanning) .on_click(cx.listener(|this, _, _, cx| this.scan_source_control(cx))); let version_control = Section::new("Version Control") .id("source-control") .header_action(rescan) .row(tool_row("Git", Lucide::GitBranch, self.source_control.git.as_ref())) .render(cx); let providers = Section::new("Source Control Providers") .row(tool_row("GitHub", Lucide::GitPullRequest, self.source_control.github.as_ref())) .render(cx); let mut naming = Section::new("Text generation").id("source-control-text-generation").row( SettingRow::new("Worktree branch naming") .id("worktree-branch-naming") .description("Choose how new worktree branches are named from your first message.") .when(saved.branch_naming == BranchNaming::Semantic, |row| { row.status( "The model chooses a prefix that describes the work, such as feat/add-search, fix/login-timeout, or refactor/auth.", ) }) .reset(reset(&view, |s| s.branch_naming, |s, naming| s.branch_naming = naming)) .control(choice("branch-naming", saved.branch_naming, BRANCH_NAMING, &view, |settings, naming| { settings.branch_naming = naming })), ); match saved.branch_naming { BranchNaming::Static => { naming = naming.row( SettingRow::new("Branch prefix") .id("branch-prefix") .description("For example, vectrum or vectrum/ produces vectrum/add-search. Leave empty for no prefix.") .reset(reset(&view, |s| s.branch_prefix.clone(), |s, prefix| s.branch_prefix = prefix)) .control(text_field(&self.branch_prefix, 224., window, cx)), ); } BranchNaming::Custom => { naming = naming.row( SettingRow::new("Branch naming instructions") .id("branch-naming-instructions") .description( "Appended to the naming prompt. The model returns the complete branch name; no prefix or suffix is added.", ) .reset(reset(&view, |s| s.branch_instructions.clone(), |s, text| s.branch_instructions = text)) .below( div() .px(px(2.)) .py(px(2.)) .rounded(px(12.)) .well(p) .text_size(px(12.5)) .text_color(p.ink) .child(Textarea::new(&self.branch_instructions).appearance(false)), ), ); } BranchNaming::Semantic => {} } vec![version_control, providers, naming.render(cx)] } }