Skip to content
Merged

Rust #16

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
133 changes: 127 additions & 6 deletions crates/mint-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use std::{
};

use mint_core::{
CHAT_CLI_ID, Capability, ChatRequest, CodeEdit, CodePatchHunk, KnowledgeStore, MemoryStore,
MintConfig, TaskStore, apply_code_edits, assert_path_capability, build_code_patch,
CHAT_CLI_ID, Capability, ChatRequest, CodeEdit, CodePatchHunk, ImageGenRequest, 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,
fetch_github_repo_summary, find_paths, index_semantic_code, initialize_config,
inspect_code_plan, list_code_files, load_config, native_plugins,
fetch_github_repo_summary, find_paths, generate_images, 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,
Expand Down Expand Up @@ -180,6 +180,23 @@ enum Command {
Onboard,
/// Interactively manage enabled agent tools.
Setup,
/// Generate an image from a text prompt using NanoBanana (Gemini image model).
Imagine {
/// Text description of the image to generate.
prompt: String,
/// Aspect ratio: 1:1, 16:9, 9:16, or 4:3 [default: 1:1]
#[arg(long, default_value = "1:1")]
aspect: String,
/// Number of images to generate (1–4) [default: 1]
#[arg(long, default_value_t = 1)]
count: u8,
/// Negative prompt — elements to avoid in the image
#[arg(long)]
negative: Option<String>,
/// Save the first generated image to this path
#[arg(long)]
output: Option<PathBuf>,
},
}

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -926,6 +943,57 @@ async fn main() -> Result<()> {
launch_mint_target(target).await?;
}
}
Command::Imagine {
prompt,
aspect,
count,
negative,
output,
} => {
let config = load_config()?;
let count = count.clamp(1, 4);
eprint!("{DIM}✦ Generating {count} image(s)...{RESET}");
let _ = std::io::stderr().flush();
let request = ImageGenRequest {
prompt: prompt.clone(),
negative_prompt: negative,
aspect_ratio: Some(aspect),
num_images: Some(count),
model: None,
provider: None,
};
match generate_images(&config, &request).await {
Ok(result) => {
eprintln!("\r{MINT}✦ Generated {} image(s) {RESET}", result.images.len());
let data_uris: Vec<String> =
result.images.iter().map(|img| img.data_uri.clone()).collect();
match mint_core::save_chat_images(data_uris, Some(result.provider.clone()), Some(prompt.clone())) {
Ok(saved) => {
for entry in &saved {
println!("{MINT}✓{RESET} Saved: {}", entry.path.display());
}
// If --output specified, copy first image there
if let (Some(out_path), Some(first)) = (&output, saved.first()) {
match std::fs::copy(&first.path, out_path) {
Ok(_) => println!("{MINT}✓{RESET} Copied to: {}", out_path.display()),
Err(e) => eprintln!("{WARN}Warning: could not copy to output path: {e}{RESET}"),
}
}
if let Some(desc) = &result.description {
if !desc.is_empty() {
println!("\n{DIM}{desc}{RESET}");
}
}
}
Err(e) => eprintln!("{ERROR}Failed to save images: {e}{RESET}"),
}
}
Err(e) => {
eprintln!("{ERROR}✗ Image generation failed: {e}{RESET}");
anyhow::bail!("image generation failed: {e}");
}
}
}
},
}
Ok(())
Expand Down Expand Up @@ -1214,6 +1282,7 @@ async fn handle_slash_command(
("/memory clear", "Clear all interactions"),
("/memory get <key>", "Read a profile value"),
("/memory set <key> <val>", "Store a profile value"),
("/image-provider [name]", "List image gen providers or switch default provider"),
("/mcp list", "List configured MCP servers"),
("/mcp allow <server> <tool>", "Allow an MCP tool"),
("/stats", "Show session statistics"),
Expand Down Expand Up @@ -1270,6 +1339,57 @@ async fn handle_slash_command(
Some(SlashResult::Handled)
}

"/image-provider" => {
let mut available = Vec::new();
if !session.config.api_key.trim().is_empty() {
available.push("nanobanana");
}
if !session.config.openai_api_key.trim().is_empty() {
available.push("dalle");
}
if !session.config.stability_api_key.trim().is_empty() {
available.push("stability");
}
if !session.config.ideogram_api_key.trim().is_empty() {
available.push("ideogram");
}
if !session.config.replicate_api_key.trim().is_empty() {
available.push("replicate");
}
if available.is_empty() {
available.push("nanobanana");
}

if rest.is_empty() {
println!("\n{BLUE}Configured image generation providers:{RESET}");
for p in &available {
let active = if p == &session.config.image_gen_provider.as_str() {
format!(" {MINT}← active{RESET}")
} else {
String::new()
};
println!(" {p}{active}");
}
println!();
} else {
if available.contains(&rest) {
session.config.image_gen_provider = rest.to_owned();
match mint_core::save_config(&session.config) {
Ok(()) => println!(
"{DIM}Switched default image provider to: {}{RESET}\n",
session.config.image_gen_provider
),
Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
}
} else {
println!(
"{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n"
);
}
}
Some(SlashResult::Handled)
}

"/clear" | "/reset" => {
println!("Clear conversation history? [y/N] ");
if let Ok(true) = confirm("Clear conversation history? [y/N] ") {
Expand Down Expand Up @@ -1884,6 +2004,7 @@ const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
("/help", "Show help menu"),
("/fast", "Toggle fast mode (hide thinking traces)"),
("/models", "List AI providers or switch active provider"),
("/image-provider", "List image gen providers or switch default provider"),
("/clear", "Clear conversation history"),
("/cd", "Change active workspace directory"),
("/image", "Attach image from disk"),
Expand Down Expand Up @@ -1963,9 +2084,9 @@ fn draw_input_box(
let highlight_idx = tab_index.map(|idx| idx % matches.len());
for (i, (cmd, desc)) in matches.iter().enumerate() {
if Some(i) == highlight_idx {
println!(" {BLUE}▶ {:<12}{RESET} {DIM}- {}{RESET}", cmd, desc);
println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
} else {
println!(" {DIM}{:<12} - {}{RESET}", cmd, desc);
println!(" {DIM}{:<16} - {}{RESET}", cmd, desc);
}
}
}
Expand Down
151 changes: 150 additions & 1 deletion crates/mint-cli/src/onboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,38 @@ const HUGGINGFACE_MODEL_PRESETS: &[&str] = &[
"google/gemma-3-27b-it",
];

// ── Image Generation Providers ──────────────────────────────────────────────
const NANOBANANA_IMAGE_MODEL_PRESETS: &[&str] = &[
"gemini-2.5-flash-image",
"gemini-2.0-flash-image",
];

const DALLE_MODEL_PRESETS: &[&str] = &[
"dall-e-3",
"gpt-image-1",
"dall-e-2",
];

const STABILITY_MODEL_PRESETS: &[&str] = &[
"sd3.5-large",
"sd3.5-large-turbo",
"sd3-medium",
"core",
];

const IDEOGRAM_MODEL_PRESETS: &[&str] = &[
"V_3",
"V_2",
"V_2_TURBO",
];

const REPLICATE_MODEL_PRESETS: &[&str] = &[
"black-forest-labs/flux-1.1-pro",
"black-forest-labs/flux-schnell",
"stability-ai/sdxl",
"bytedance/sdxl-lightning-4step",
];

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

Expand Down Expand Up @@ -231,6 +263,37 @@ pub async fn run() -> Result<()> {
.and_then(|v| v.as_bool())
.unwrap_or(false),
},
// ── Image Generation ─────────────────────────────────────────────────
OnboardService {
category: "Image Generation",
name: "Google NanoBanana (Gemini Images) [uses Gemini key]",
key: "img_nanobanana",
enabled: !config.api_key.is_empty() && !config.nanobanana_model.is_empty(),
},
OnboardService {
category: "Image Generation",
name: "OpenAI DALL·E [uses OpenAI key]",
key: "img_dalle",
enabled: !config.openai_api_key.is_empty() && !config.dalle_model.is_empty(),
},
OnboardService {
category: "Image Generation",
name: "Stability AI (Stable Diffusion)",
key: "img_stability",
enabled: !config.stability_api_key.is_empty(),
},
OnboardService {
category: "Image Generation",
name: "Ideogram v3",
key: "img_ideogram",
enabled: !config.ideogram_api_key.is_empty(),
},
OnboardService {
category: "Image Generation",
name: "Replicate (FLUX / SDXL / custom)",
key: "img_replicate",
enabled: !config.replicate_api_key.is_empty(),
},
];

let mut cursor = 0;
Expand Down Expand Up @@ -842,7 +905,93 @@ pub async fn run() -> Result<()> {
);
}

println!();
// ────────────────────────────────────────────────────────────────────────
// Image Generation providers
// ────────────────────────────────────────────────────────────────────────

// NanoBanana (Gemini Images)
if is_selected("img_nanobanana", &services) {
println!("\n\x1b[36m--- Google NanoBanana (Gemini Images) ---\x1b[0m");
println!(
"\x1b[90mUses the same Gemini API key as Step 1. Select the image generation model.\x1b[0m"
);
config.nanobanana_model = prompt_select_or_custom(
"NanoBanana Model",
static_model_options(NANOBANANA_IMAGE_MODEL_PRESETS),
Some(&config.nanobanana_model),
"Custom NanoBanana model...",
)?;
}

// DALL·E
if is_selected("img_dalle", &services) {
println!("\n\x1b[36m--- OpenAI DALL·E ---\x1b[0m");
println!(
"\x1b[90mUses the same OpenAI API key. DALL·E 3 supports only 1 image per request; DALL·E 2 supports up to 10.\x1b[0m"
);
config.dalle_model = prompt_select_or_custom(
"DALL·E Model",
static_model_options(DALLE_MODEL_PRESETS),
Some(&config.dalle_model),
"Custom DALL·E model...",
)?;
}

// Stability AI
if is_selected("img_stability", &services) {
println!("\n\x1b[36m--- Stability AI ---\x1b[0m");
println!(
"\x1b[90mGet your API key at https://platform.stability.ai/. Supports SD3.5 Large, SD3 Medium, and Stable Image Core.\x1b[0m"
);
config.stability_api_key =
prompt_sensitive("Stability AI API Key", &config.stability_api_key)?;
config.stability_model = prompt_select_or_custom(
"Stability Model",
static_model_options(STABILITY_MODEL_PRESETS),
Some(&config.stability_model),
"Custom Stability model...",
)?;
} else {
config.stability_api_key = String::new();
}

// Ideogram
if is_selected("img_ideogram", &services) {
println!("\n\x1b[36m--- Ideogram ---\x1b[0m");
println!(
"\x1b[90mGet your API key at https://ideogram.ai/api. Supports V_3, V_2, and V_2_TURBO.\x1b[0m"
);
config.ideogram_api_key =
prompt_sensitive("Ideogram API Key", &config.ideogram_api_key)?;
config.ideogram_model = prompt_select_or_custom(
"Ideogram Model",
static_model_options(IDEOGRAM_MODEL_PRESETS),
Some(&config.ideogram_model),
"Custom Ideogram model...",
)?;
} else {
config.ideogram_api_key = String::new();
}

// Replicate
if is_selected("img_replicate", &services) {
println!("\n\x1b[36m--- Replicate ---\x1b[0m");
println!(
"\x1b[90mGet your API token at https://replicate.com/account/api-tokens. Works with FLUX, SDXL, and any public image model.\x1b[0m"
);
config.replicate_api_key =
prompt_sensitive("Replicate API Token", &config.replicate_api_key)?;
config.replicate_model = prompt_select_or_custom(
"Replicate Model (owner/model-name)",
static_model_options(REPLICATE_MODEL_PRESETS),
Some(&config.replicate_model),
"Custom Replicate model...",
)?;
} else {
config.replicate_api_key = String::new();
}


save_config(&config)?;
println!("\x1b[32m✅ Configuration saved successfully!\x1b[0m");
Ok(())
Expand Down
Loading
Loading