Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,11 @@ jobs:
- name: Build desktop bundle
run: npm run tauri:build

- name: Build CLI
run: npm run build:cli

- name: Collect artifacts
run: |
mkdir -p release-artifacts
cp target/release/bundle/deb/*.deb release-artifacts/
cp target/release/bundle/tar/*.tar.gz release-artifacts/
cp target/release/mint release-artifacts/mint-cli-linux-x86_64
sha256sum release-artifacts/* > release-artifacts/SHA256SUMS

- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
Expand Down
10 changes: 5 additions & 5 deletions crates/mint-cli/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,15 +787,15 @@ fn confirm_pausing_interrupt(prompt: &str, approval_active: &AtomicBool) -> bool
fn format_markdown_bold(text: &str) -> String {
let mut formatted_lines = Vec::new();
let mut in_code_block = false;

for line in text.lines() {
let mut formatted_line = line.to_string();
let trimmed = line.trim_start();

if trimmed.starts_with("```") {
in_code_block = !in_code_block;
}

if !in_code_block {
let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
if hash_count > 0 && hash_count <= 6 {
Expand All @@ -818,10 +818,10 @@ fn format_markdown_bold(text: &str) -> String {
// Do not format markdown inside code blocks
formatted_line = line.to_string();
}

formatted_lines.push(formatted_line);
}

let mut result = formatted_lines.join("\n");
if text.ends_with('\n') {
result.push('\n');
Expand Down
32 changes: 20 additions & 12 deletions crates/mint-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use mint_core::{
CHAT_CLI_ID, Capability, ChatRequest, CodeEdit, CodePatchHunk, KnowledgeStore, MemoryStore,
MintConfig, TaskStore, apply_code_edits, assert_path_capability, build_code_patch,
build_symbol_index, classify_shell_command, config_path, create_folder, execute_native_plugin,
find_paths, index_semantic_code, initialize_config, inspect_code_plan, list_code_files,
load_config, native_plugins, orchestrate_chat_stream_with_fallback,
orchestrate_chat_with_fallback, propose_code_edits, read_code_file, repository_summary,
run_shell_command, search_code, search_semantic_code, set_config_value,
parse_github_url, fetch_github_repo_summary,
fetch_github_repo_summary, find_paths, index_semantic_code, initialize_config,
inspect_code_plan, list_code_files, load_config, native_plugins,
orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback, parse_github_url,
propose_code_edits, read_code_file, repository_summary, run_shell_command, search_code,
search_semantic_code, set_config_value,
};

mod agent;
Expand Down Expand Up @@ -2746,18 +2746,24 @@ fn print_shell_output(output: &mint_core::ShellOutput) {
);
}



async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
let Some((owner, repo_name)) = parse_github_url(repo) else {
anyhow::bail!("Invalid GitHub repository URL/format. Please use 'owner/repo' or a full GitHub URL.");
anyhow::bail!(
"Invalid GitHub repository URL/format. Please use 'owner/repo' or a full GitHub URL."
);
};

println!("Fetching information for {}/{} from GitHub...", owner, repo_name);
println!(
"Fetching information for {}/{} from GitHub...",
owner, repo_name
);
let summary = match fetch_github_repo_summary(&owner, &repo_name).await {
Ok(s) => s,
Err(e) => {
anyhow::bail!("Failed to fetch repository summary: {}. Check that the repository is public and spelled correctly.", e);
anyhow::bail!(
"Failed to fetch repository summary: {}. Check that the repository is public and spelled correctly.",
e
);
}
};

Expand All @@ -2781,13 +2787,15 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
)
.await?;

println!("\n--- AI Repository Overview for {}/{} ---", owner, repo_name);
println!(
"\n--- AI Repository Overview for {}/{} ---",
owner, repo_name
);
println!("{}", response.text);
println!("--------------------------------------------------");
Ok(())
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down
28 changes: 20 additions & 8 deletions crates/mint-cli/src/updater.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::process::Command;
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result, bail};
Expand Down Expand Up @@ -165,24 +165,36 @@ pub fn print_update_notice(current: &str, latest: &str) {
let notes_url = "https://github.com/Pheem49/Mint/releases/latest";

// Text lengths (including 1 leading space)
let title_clean_len = 1 + 3 + format!("Update available! {} -> {}", current, latest).chars().count();
let title_clean_len = 1
+ 3
+ format!("Update available! {} -> {}", current, latest)
.chars()
.count();
let command_msg_len = 1 + command_msg.len();
let notes_label_len = 1 + notes_label.len();
let notes_url_len = 1 + notes_url.len();

let max_len = command_msg_len
.max(title_clean_len)
.max(notes_label_len)
.max(notes_url_len) + 1; // plus 1 for trailing space before right border
.max(notes_url_len)
+ 1; // plus 1 for trailing space before right border

let border = "─".repeat(max_len);
println!("\x1b[33m╭{}╮\x1b[0m", border);

// Line 1: Title
let title_display = format!(" ✨ Update available! \x1b[1;32m{}\x1b[0;33m -> \x1b[1;32m{}\x1b[0;33m", current, latest);
let title_display = format!(
" ✨ Update available! \x1b[1;32m{}\x1b[0;33m -> \x1b[1;32m{}\x1b[0;33m",
current, latest
);
let padding1 = max_len - title_clean_len;
println!("\x1b[33m│\x1b[0m{}{}\x1b[33m│\x1b[0m", title_display, " ".repeat(padding1));

println!(
"\x1b[33m│\x1b[0m{}{}\x1b[33m│\x1b[0m",
title_display,
" ".repeat(padding1)
);

// Line 2: Command
let padding2 = max_len - command_msg_len;
println!(
Expand All @@ -209,7 +221,7 @@ pub fn print_update_notice(current: &str, latest: &str) {
notes_url,
" ".repeat(padding5)
);

println!("\x1b[33m╰{}╯\x1b[0m\n", border);
}

Expand Down
36 changes: 27 additions & 9 deletions crates/mint-core/src/code_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,13 @@ fn is_ignored_directory(path: &Path) -> bool {
}

pub fn parse_github_url(url: &str) -> Option<(String, String)> {
let cleaned = url.trim()
let cleaned = url
.trim()
.trim_start_matches("https://")
.trim_start_matches("http://")
.trim_start_matches("www.")
.trim_start_matches("github.com/");

let parts: Vec<&str> = cleaned.split('/').collect();
if parts.len() >= 2 {
let owner = parts[0].to_string();
Expand All @@ -493,17 +494,26 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String

// 1. Fetch Repository Info
let repo_url = format!("https://api.github.com/repos/{}/{}", owner, repo);
let repo_resp = client.get(&repo_url).send().await.map_err(|e| e.to_string())?;
let repo_resp = client
.get(&repo_url)
.send()
.await
.map_err(|e| e.to_string())?;
if !repo_resp.status().is_success() {
return Err(format!("Failed to fetch repository metadata: {}", repo_resp.status()));
return Err(format!(
"Failed to fetch repository metadata: {}",
repo_resp.status()
));
}
let repo_info: serde_json::Value = repo_resp.json().await.map_err(|e| e.to_string())?;

let description = repo_info["description"].as_str().unwrap_or("No description provided.");

let description = repo_info["description"]
.as_str()
.unwrap_or("No description provided.");
let language = repo_info["language"].as_str().unwrap_or("Unknown");
let stars = repo_info["stargazers_count"].as_u64().unwrap_or(0);
let forks = repo_info["forks_count"].as_u64().unwrap_or(0);

let mut topics_list = Vec::new();
if let Some(topics) = repo_info["topics"].as_array() {
for t in topics {
Expand All @@ -520,7 +530,11 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String

// 2. Fetch Directory contents (top level)
let contents_url = format!("https://api.github.com/repos/{}/{}/contents", owner, repo);
let contents_resp = client.get(&contents_url).send().await.map_err(|e| e.to_string())?;
let contents_resp = client
.get(&contents_url)
.send()
.await
.map_err(|e| e.to_string())?;
let mut file_tree = String::from("Unavailable");
if contents_resp.status().is_success() {
if let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await {
Expand All @@ -538,7 +552,11 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String

// 3. Fetch README.md
let readme_url = format!("https://api.github.com/repos/{}/{}/readme", owner, repo);
let readme_resp = client.get(&readme_url).send().await.map_err(|e| e.to_string())?;
let readme_resp = client
.get(&readme_url)
.send()
.await
.map_err(|e| e.to_string())?;
let mut readme_text = String::from("No README available.");
if readme_resp.status().is_success() {
if let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await {
Expand Down
4 changes: 2 additions & 2 deletions crates/mint-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub use chat::{
pub use code_tools::{
AppliedCodeEdit, CodeEdit, CodeEditPreview, CodeEditProposal, CodeFile, CodeInspectionError,
CodePatchHunk, CodePlan, CodeSearchHit, RepositorySummary, apply_code_edits, build_code_patch,
inspect_code_plan, list_code_files, propose_code_edits, read_code_file, repository_summary,
search_code, parse_github_url, fetch_github_repo_summary,
fetch_github_repo_summary, inspect_code_plan, list_code_files, parse_github_url,
propose_code_edits, read_code_file, repository_summary, search_code,
};
pub use config::{
ConfigError, MintConfig, config_path, initialize_config, load_config, save_config,
Expand Down
18 changes: 12 additions & 6 deletions crates/mint-core/src/orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ pub async fn resolve_github_links(message: &str, config: &MintConfig) -> String
use std::sync::OnceLock;
static RE: OnceLock<regex::Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
regex::Regex::new(r"https?://(?:www\.)?github\.com/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)").unwrap()
regex::Regex::new(r"https?://(?:www\.)?github\.com/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)")
.unwrap()
});

let mut resolved_msg = message.to_string();
Expand All @@ -64,11 +65,16 @@ pub async fn resolve_github_links(message: &str, config: &MintConfig) -> String
if repo.ends_with(".git") {
repo = repo[..repo.len() - 4].to_string();
}
let repo_clean: String = repo.chars().take_while(|c| c.is_alphanumeric() || *c == '-' || *c == '_' || *c == '.').collect();

let repo_clean: String = repo
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '-' || *c == '_' || *c == '.')
.collect();

let repo_key = format!("{owner}/{repo_clean}");
if resolved_repos.insert(repo_key.clone()) {
if let Ok(summary) = crate::code_tools::fetch_github_repo_summary(owner, &repo_clean).await {
if let Ok(summary) =
crate::code_tools::fetch_github_repo_summary(owner, &repo_clean).await
{
resolved_msg.push_str(&format!(
"\n\n--- Auto-Resolved GitHub Metadata for {} ---\n{}\n--------------------------------------------",
repo_key, summary
Expand Down Expand Up @@ -2075,8 +2081,8 @@ mod tests {
assert_eq!(decision.action, "finish");
assert!(decision.input.summary.is_empty());

let decision = parse_decision(r#"{"thought":"done"}"#)
.expect("missing finish should parse");
let decision =
parse_decision(r#"{"thought":"done"}"#).expect("missing finish should parse");
assert_eq!(decision.action, "finish");
assert!(decision.input.summary.is_empty());
}
Expand Down
Loading