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
27 changes: 27 additions & 0 deletions crates/mint-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1605,7 +1605,34 @@ async fn run_interactive_chat() -> Result<()> {
pending_image: None,
};

let mut printed_update = false;
if let Some((current, latest)) = updater::get_cached_update_notice() {
updater::print_update_notice(&current, &latest);
printed_update = true;
}

let mut update_handle = if updater::should_check_for_update() {
Some(tokio::task::spawn_blocking(
updater::check_for_update_quietly,
))
} else {
None
};

loop {
if let Some(handle) = update_handle.take() {
if handle.is_finished() {
if let Ok(Some((current, latest))) = handle.await {
if !printed_update {
updater::print_update_notice(&current, &latest);
printed_update = true;
}
}
} else {
update_handle = Some(handle);
}
}

let path_str = format_path_with_tilde(&session.current_dir);
let model_str = active_model(&session.config.ai_provider, &session.config).to_owned();

Expand Down
25 changes: 18 additions & 7 deletions crates/mint-cli/src/onboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use mint_core::{load_config, save_config};
use std::io::{self, Write};
use std::process::Command;
use std::net::TcpStream;
use std::process::Command;

struct OnboardService {
category: &'static str,
Expand Down Expand Up @@ -60,7 +60,6 @@ const HUGGINGFACE_MODEL_PRESETS: &[&str] = &[
"google/gemma-3-27b-it",
];


pub async fn run() -> Result<()> {
let mut config = load_config()?;

Expand Down Expand Up @@ -1017,12 +1016,17 @@ fn ensure_ollama_serving(host: &str) {

// Check if Ollama is already serving
if TcpStream::connect_timeout(
&addr.parse().unwrap_or_else(|_| "127.0.0.1:11434".parse().unwrap()),
&addr
.parse()
.unwrap_or_else(|_| "127.0.0.1:11434".parse().unwrap()),
std::time::Duration::from_secs(1),
)
.is_ok()
{
println!("\x1b[32m✔ Ollama server is already running at {}\x1b[0m", host);
println!(
"\x1b[32m✔ Ollama server is already running at {}\x1b[0m",
host
);
return;
}

Expand All @@ -1042,7 +1046,9 @@ fn ensure_ollama_serving(host: &str) {
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(500));
if TcpStream::connect_timeout(
&addr.parse().unwrap_or_else(|_| "127.0.0.1:11434".parse().unwrap()),
&addr
.parse()
.unwrap_or_else(|_| "127.0.0.1:11434".parse().unwrap()),
std::time::Duration::from_secs(1),
)
.is_ok()
Expand All @@ -1052,9 +1058,14 @@ fn ensure_ollama_serving(host: &str) {
}
}
if ready {
println!("\r\x1b[32m✔ Ollama server started successfully at {}\x1b[0m ", host);
println!(
"\r\x1b[32m✔ Ollama server started successfully at {}\x1b[0m ",
host
);
} else {
println!("\r\x1b[31m✘ Ollama server started but not responding yet. It may need more time.\x1b[0m");
println!(
"\r\x1b[31m✘ Ollama server started but not responding yet. It may need more time.\x1b[0m"
);
}
}
Err(e) => {
Expand Down
167 changes: 167 additions & 0 deletions crates/mint-cli/src/updater.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::process::Command;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result, bail};

Expand All @@ -20,6 +22,7 @@ pub fn run(check_only: bool, dry_run: bool, approved: bool) -> Result<()> {
.trim()
.trim_matches('"')
.to_owned();
write_cache(&latest);
if compare_versions(current, &latest) >= 0 {
println!("Mint is already up to date ({current}).");
return Ok(());
Expand Down Expand Up @@ -61,6 +64,155 @@ fn npm() -> &'static str {
}
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct UpdateCache {
last_checked: u64,
latest_version: String,
}

fn cache_path() -> Option<PathBuf> {
#[cfg(test)]
{
Some(std::env::temp_dir().join("update-cache.json"))
}
#[cfg(not(test))]
{
dirs::config_dir().map(|dir| dir.join("mint").join("update-cache.json"))
}
}

fn get_current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}

fn write_cache(latest_version: &str) -> Option<()> {
let path = cache_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok()?;
}
let cache = UpdateCache {
last_checked: get_current_timestamp(),
latest_version: latest_version.to_owned(),
};
let json = serde_json::to_string_pretty(&cache).ok()?;
std::fs::write(path, json).ok()?;
Some(())
}

pub fn get_cached_update_notice() -> Option<(String, String)> {
let path = cache_path()?;
if !path.exists() {
return None;
}
let data = std::fs::read_to_string(path).ok()?;
let cache: UpdateCache = serde_json::from_str(&data).ok()?;
let current = env!("CARGO_PKG_VERSION");
if compare_versions(current, &cache.latest_version) < 0 {
Some((current.to_owned(), cache.latest_version))
} else {
None
}
}

pub fn should_check_for_update() -> bool {
let path = match cache_path() {
Some(p) => p,
None => return false,
};
if !path.exists() {
return true;
}
let data = match std::fs::read_to_string(path) {
Ok(d) => d,
Err(_) => return true,
};
let cache: UpdateCache = match serde_json::from_str(&data) {
Ok(c) => c,
Err(_) => return true,
};
let now = get_current_timestamp();
// Check once every 24 hours (86400 seconds)
now.saturating_sub(cache.last_checked) > 86400
}

pub fn check_for_update_quietly() -> Option<(String, String)> {
let current = env!("CARGO_PKG_VERSION");
let output = Command::new(npm())
.args(["view", PACKAGE, "version", "--json"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let latest = String::from_utf8_lossy(&output.stdout)
.trim()
.trim_matches('"')
.to_owned();
write_cache(&latest);
if compare_versions(current, &latest) < 0 {
Some((current.to_owned(), latest))
} else {
None
}
}

pub fn print_update_notice(current: &str, latest: &str) {
let command_msg = "Run sh -c 'curl -fsSL https://raw.githubusercontent.com/Pheem49/Mint/main/install.sh | MINT_NON_INTERACTIVE=1 sh' to update.";
let notes_label = "See full release notes:";
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 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

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 padding1 = max_len - title_clean_len;
println!("\x1b[33m│\x1b[0m{}{}\x1b[33m│\x1b[0m", title_display, " ".repeat(padding1));

// Line 2: Command
let padding2 = max_len - command_msg_len;
println!(
"\x1b[33m│\x1b[0m \x1b[37m{}\x1b[0m{}\x1b[33m│\x1b[0m",
command_msg,
" ".repeat(padding2)
);

// Line 3: Empty separator
println!("\x1b[33m│\x1b[0m{}\x1b[33m│\x1b[0m", " ".repeat(max_len));

// Line 4: Notes label
let padding4 = max_len - notes_label_len;
println!(
"\x1b[33m│\x1b[0m \x1b[90m{}\x1b[0m{}\x1b[33m│\x1b[0m",
notes_label,
" ".repeat(padding4)
);

// Line 5: Notes URL
let padding5 = max_len - notes_url_len;
println!(
"\x1b[33m│\x1b[0m \x1b[36m{}\x1b[0m{}\x1b[33m│\x1b[0m",
notes_url,
" ".repeat(padding5)
);

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

fn compare_versions(left: &str, right: &str) -> i8 {
let parse = |value: &str| {
value
Expand Down Expand Up @@ -97,4 +249,19 @@ mod tests {
assert_eq!(compare_versions("1.5.4", "1.6.0"), -1);
assert_eq!(compare_versions("v2.0.0-alpha.1", "2.0.0"), 0);
}

#[test]
fn test_write_and_read_cache() {
let path = cache_path().unwrap();
if path.exists() {
let _ = std::fs::remove_file(&path);
}
assert!(get_cached_update_notice().is_none());
assert!(write_cache("99.9.9").is_some());
let notice = get_cached_update_notice();
assert!(notice.is_some());
let (_current, latest) = notice.unwrap();
assert_eq!(latest, "99.9.9");
let _ = std::fs::remove_file(&path);
}
}
5 changes: 4 additions & 1 deletion crates/mint-core/src/api_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = TcpListener::bind(addr).await?;
println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
println!("\x1b[32m Mint Local API Server running at http://{}\x1b[0m", addr);
println!(
"\x1b[32m Mint Local API Server running at http://{}\x1b[0m",
addr
);
println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");

// Start background messaging bridges (Telegram, Discord, Slack)
Expand Down
2 changes: 1 addition & 1 deletion crates/mint-core/src/channels.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::time::Duration;

use futures_util::{SinkExt, StreamExt};
use crate::{ChatRequest, MintConfig, load_config, orchestrate_chat};
use futures_util::{SinkExt, StreamExt};
use serde_json::{Value, json};
use tokio_tungstenite::{connect_async, tungstenite::Message};

Expand Down
6 changes: 5 additions & 1 deletion crates/mint-core/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,11 @@ fn ollama_images(request: &ChatRequest) -> Option<Vec<String>> {
.map(|(_, data)| data.to_owned())
})
.collect();
if images.is_empty() { None } else { Some(images) }
if images.is_empty() {
None
} else {
Some(images)
}
}

fn wants_agent_json(request: &ChatRequest) -> bool {
Expand Down
9 changes: 8 additions & 1 deletion crates/mint-core/tests/memory_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ fn stores_recent_interactions_with_provider_metadata() {
.add_interaction_with_metadata("hello", "hi", "gemini", "gemini-test")
.unwrap();
store
.add_interaction_for_chat_with_fallback("", "question", "answer", "gemini", "gemini-test", Some("ollama"))
.add_interaction_for_chat_with_fallback(
"",
"question",
"answer",
"gemini",
"gemini-test",
Some("ollama"),
)
.unwrap();

let interactions = store.recent_interactions(2).unwrap();
Expand Down
Loading