@pheem49/mint 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +82 -21
  4. package/assets/Mint_CLI.png +0 -0
  5. package/assets/Mint_Desktop.png +0 -0
  6. package/assets/Mint_Web.png +0 -0
  7. package/assets/Mint_WorkSpace.png +0 -0
  8. package/assets/Setting.png +0 -0
  9. package/assets/bridges.svg +4 -0
  10. package/assets/chat.svg +3 -0
  11. package/assets/code.svg +4 -0
  12. package/assets/features.svg +3 -0
  13. package/assets/imagegen.svg +5 -0
  14. package/assets/live2d.svg +6 -0
  15. package/assets/memory.svg +5 -0
  16. package/assets/screencapture.svg +4 -0
  17. package/assets/setup.svg +3 -0
  18. package/assets/tools.svg +4 -0
  19. package/bin/mint +0 -0
  20. package/crates/mint-cli/src/agent.rs +47 -15
  21. package/crates/mint-cli/src/main.rs +534 -7
  22. package/crates/mint-cli/src/onboard.rs +134 -1
  23. package/crates/mint-cli/src/setup.rs +173 -32
  24. package/crates/mint-core/src/api_server.rs +147 -3
  25. package/crates/mint-core/src/config.rs +22 -3
  26. package/crates/mint-core/src/image_gen.rs +895 -0
  27. package/crates/mint-core/src/lib.rs +4 -0
  28. package/crates/mint-core/src/memory.rs +27 -1
  29. package/crates/mint-core/src/orchestration.rs +1 -1
  30. package/crates/mint-core/src/plugins.rs +32 -0
  31. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  32. package/package.json +1 -1
  33. package/src/bin/index.js +0 -0
  34. package/src/renderer/src/agentProgress.ts +39 -0
  35. package/src/renderer/src/components/ChatPanel.tsx +547 -47
  36. package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
  37. package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
  38. package/src/renderer/src/components/MintDashboard.tsx +84 -31
  39. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  40. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  41. package/src/renderer/src/components/SettingsWindow.tsx +29 -20
  42. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  43. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  44. package/src/renderer/src/css/image-studio.css +722 -0
  45. package/src/renderer/src/css/settings.css +13 -3
  46. package/src/renderer/src/css/styles.css +245 -43
  47. package/src/renderer/src/index.css +152 -0
  48. package/src/renderer/src/tauri.ts +148 -0
  49. package/src/renderer/src-web/App.tsx +16 -1
  50. package/src/renderer/src-web/agentProgress.ts +39 -0
  51. package/src/renderer/src-web/components/ChatPanel.tsx +568 -46
  52. package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
  53. package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
  54. package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
  55. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  56. package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
  57. package/src/renderer/src-web/css/image-studio.css +722 -0
  58. package/src/renderer/src-web/css/settings.css +14 -4
  59. package/src/renderer/src-web/css/styles.css +292 -44
  60. package/src/renderer/src-web/index.css +152 -0
  61. package/src/renderer/src-web/tauri.ts +149 -0
  62. package/src-tauri/src/lib.rs +65 -5
  63. package/src-tauri/tauri.conf.json +1 -1
@@ -60,6 +60,24 @@ const HUGGINGFACE_MODEL_PRESETS: &[&str] = &[
60
60
  "google/gemma-3-27b-it",
61
61
  ];
62
62
 
63
+ // ── Image Generation Providers ──────────────────────────────────────────────
64
+ const NANOBANANA_IMAGE_MODEL_PRESETS: &[&str] =
65
+ &["gemini-2.5-flash-image", "gemini-2.0-flash-image"];
66
+
67
+ const DALLE_MODEL_PRESETS: &[&str] = &["dall-e-3", "gpt-image-1", "dall-e-2"];
68
+
69
+ const STABILITY_MODEL_PRESETS: &[&str] =
70
+ &["sd3.5-large", "sd3.5-large-turbo", "sd3-medium", "core"];
71
+
72
+ const IDEOGRAM_MODEL_PRESETS: &[&str] = &["V_3", "V_2", "V_2_TURBO"];
73
+
74
+ const REPLICATE_MODEL_PRESETS: &[&str] = &[
75
+ "black-forest-labs/flux-1.1-pro",
76
+ "black-forest-labs/flux-schnell",
77
+ "stability-ai/sdxl",
78
+ "bytedance/sdxl-lightning-4step",
79
+ ];
80
+
63
81
  pub async fn run() -> Result<()> {
64
82
  let mut config = load_config()?;
65
83
 
@@ -231,6 +249,37 @@ pub async fn run() -> Result<()> {
231
249
  .and_then(|v| v.as_bool())
232
250
  .unwrap_or(false),
233
251
  },
252
+ // ── Image Generation ─────────────────────────────────────────────────
253
+ OnboardService {
254
+ category: "Image Generation",
255
+ name: "Google NanoBanana (Gemini Images) [uses Gemini key]",
256
+ key: "img_nanobanana",
257
+ enabled: !config.api_key.is_empty() && !config.nanobanana_model.is_empty(),
258
+ },
259
+ OnboardService {
260
+ category: "Image Generation",
261
+ name: "OpenAI DALL·E [uses OpenAI key]",
262
+ key: "img_dalle",
263
+ enabled: !config.openai_api_key.is_empty() && !config.dalle_model.is_empty(),
264
+ },
265
+ OnboardService {
266
+ category: "Image Generation",
267
+ name: "Stability AI (Stable Diffusion)",
268
+ key: "img_stability",
269
+ enabled: !config.stability_api_key.is_empty(),
270
+ },
271
+ OnboardService {
272
+ category: "Image Generation",
273
+ name: "Ideogram v3",
274
+ key: "img_ideogram",
275
+ enabled: !config.ideogram_api_key.is_empty(),
276
+ },
277
+ OnboardService {
278
+ category: "Image Generation",
279
+ name: "Replicate (FLUX / SDXL / custom)",
280
+ key: "img_replicate",
281
+ enabled: !config.replicate_api_key.is_empty(),
282
+ },
234
283
  ];
235
284
 
236
285
  let mut cursor = 0;
@@ -842,7 +891,91 @@ pub async fn run() -> Result<()> {
842
891
  );
843
892
  }
844
893
 
845
- println!();
894
+ // ────────────────────────────────────────────────────────────────────────
895
+ // Image Generation providers
896
+ // ────────────────────────────────────────────────────────────────────────
897
+
898
+ // NanoBanana (Gemini Images)
899
+ if is_selected("img_nanobanana", &services) {
900
+ println!("\n\x1b[36m--- Google NanoBanana (Gemini Images) ---\x1b[0m");
901
+ println!(
902
+ "\x1b[90mUses the same Gemini API key as Step 1. Select the image generation model.\x1b[0m"
903
+ );
904
+ config.nanobanana_model = prompt_select_or_custom(
905
+ "NanoBanana Model",
906
+ static_model_options(NANOBANANA_IMAGE_MODEL_PRESETS),
907
+ Some(&config.nanobanana_model),
908
+ "Custom NanoBanana model...",
909
+ )?;
910
+ }
911
+
912
+ // DALL·E
913
+ if is_selected("img_dalle", &services) {
914
+ println!("\n\x1b[36m--- OpenAI DALL·E ---\x1b[0m");
915
+ println!(
916
+ "\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"
917
+ );
918
+ config.dalle_model = prompt_select_or_custom(
919
+ "DALL·E Model",
920
+ static_model_options(DALLE_MODEL_PRESETS),
921
+ Some(&config.dalle_model),
922
+ "Custom DALL·E model...",
923
+ )?;
924
+ }
925
+
926
+ // Stability AI
927
+ if is_selected("img_stability", &services) {
928
+ println!("\n\x1b[36m--- Stability AI ---\x1b[0m");
929
+ println!(
930
+ "\x1b[90mGet your API key at https://platform.stability.ai/. Supports SD3.5 Large, SD3 Medium, and Stable Image Core.\x1b[0m"
931
+ );
932
+ config.stability_api_key =
933
+ prompt_sensitive("Stability AI API Key", &config.stability_api_key)?;
934
+ config.stability_model = prompt_select_or_custom(
935
+ "Stability Model",
936
+ static_model_options(STABILITY_MODEL_PRESETS),
937
+ Some(&config.stability_model),
938
+ "Custom Stability model...",
939
+ )?;
940
+ } else {
941
+ config.stability_api_key = String::new();
942
+ }
943
+
944
+ // Ideogram
945
+ if is_selected("img_ideogram", &services) {
946
+ println!("\n\x1b[36m--- Ideogram ---\x1b[0m");
947
+ println!(
948
+ "\x1b[90mGet your API key at https://ideogram.ai/api. Supports V_3, V_2, and V_2_TURBO.\x1b[0m"
949
+ );
950
+ config.ideogram_api_key = prompt_sensitive("Ideogram API Key", &config.ideogram_api_key)?;
951
+ config.ideogram_model = prompt_select_or_custom(
952
+ "Ideogram Model",
953
+ static_model_options(IDEOGRAM_MODEL_PRESETS),
954
+ Some(&config.ideogram_model),
955
+ "Custom Ideogram model...",
956
+ )?;
957
+ } else {
958
+ config.ideogram_api_key = String::new();
959
+ }
960
+
961
+ // Replicate
962
+ if is_selected("img_replicate", &services) {
963
+ println!("\n\x1b[36m--- Replicate ---\x1b[0m");
964
+ println!(
965
+ "\x1b[90mGet your API token at https://replicate.com/account/api-tokens. Works with FLUX, SDXL, and any public image model.\x1b[0m"
966
+ );
967
+ config.replicate_api_key =
968
+ prompt_sensitive("Replicate API Token", &config.replicate_api_key)?;
969
+ config.replicate_model = prompt_select_or_custom(
970
+ "Replicate Model (owner/model-name)",
971
+ static_model_options(REPLICATE_MODEL_PRESETS),
972
+ Some(&config.replicate_model),
973
+ "Custom Replicate model...",
974
+ )?;
975
+ } else {
976
+ config.replicate_api_key = String::new();
977
+ }
978
+
846
979
  save_config(&config)?;
847
980
  println!("\x1b[32m✅ Configuration saved successfully!\x1b[0m");
848
981
  Ok(())
@@ -1,7 +1,7 @@
1
1
  use anyhow::Result;
2
2
  use crossterm::event::{self, Event, KeyCode};
3
3
  use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
4
- use mint_core::{load_config, save_config};
4
+ use mint_core::{load_config, save_config, native_plugins};
5
5
  use std::io::{self, Write};
6
6
 
7
7
  struct ToolOption {
@@ -13,15 +13,6 @@ struct ToolOption {
13
13
  pub async fn run() -> Result<Option<String>> {
14
14
  let mut config = load_config()?;
15
15
 
16
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
17
- println!("\x1b[32m Mint CLI Tool Manager Wizard\x1b[0m");
18
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
19
- println!("Configure which agent tools are enabled or disabled:");
20
- println!(
21
- " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
22
- );
23
- println!();
24
-
25
16
  let mut options = vec![
26
17
  ToolOption {
27
18
  name: "list_files (List Workspace Files)",
@@ -178,7 +169,7 @@ pub async fn run() -> Result<Option<String>> {
178
169
  ];
179
170
 
180
171
  let mut cursor = 0;
181
- print_options(&options, cursor);
172
+ redraw_phase_1(&options, cursor);
182
173
  enable_raw_mode()?;
183
174
 
184
175
  loop {
@@ -204,8 +195,7 @@ pub async fn run() -> Result<Option<String>> {
204
195
  cursor = options.len() - 1;
205
196
  }
206
197
  disable_raw_mode()?;
207
- print!("\x1b[{}A\x1b[J", options.len());
208
- print_options(&options, cursor);
198
+ redraw_phase_1(&options, cursor);
209
199
  enable_raw_mode()?;
210
200
  }
211
201
  KeyCode::Down => {
@@ -215,15 +205,13 @@ pub async fn run() -> Result<Option<String>> {
215
205
  cursor = 0;
216
206
  }
217
207
  disable_raw_mode()?;
218
- print!("\x1b[{}A\x1b[J", options.len());
219
- print_options(&options, cursor);
208
+ redraw_phase_1(&options, cursor);
220
209
  enable_raw_mode()?;
221
210
  }
222
211
  KeyCode::Char(' ') => {
223
212
  options[cursor].enabled = !options[cursor].enabled;
224
213
  disable_raw_mode()?;
225
- print!("\x1b[{}A\x1b[J", options.len());
226
- print_options(&options, cursor);
214
+ redraw_phase_1(&options, cursor);
227
215
  enable_raw_mode()?;
228
216
  }
229
217
  KeyCode::Char('a') => {
@@ -231,8 +219,7 @@ pub async fn run() -> Result<Option<String>> {
231
219
  opt.enabled = true;
232
220
  }
233
221
  disable_raw_mode()?;
234
- print!("\x1b[{}A\x1b[J", options.len());
235
- print_options(&options, cursor);
222
+ redraw_phase_1(&options, cursor);
236
223
  enable_raw_mode()?;
237
224
  }
238
225
  KeyCode::Char('i') => {
@@ -240,8 +227,7 @@ pub async fn run() -> Result<Option<String>> {
240
227
  opt.enabled = !opt.enabled;
241
228
  }
242
229
  disable_raw_mode()?;
243
- print!("\x1b[{}A\x1b[J", options.len());
244
- print_options(&options, cursor);
230
+ redraw_phase_1(&options, cursor);
245
231
  enable_raw_mode()?;
246
232
  }
247
233
  KeyCode::Enter => {
@@ -270,15 +256,135 @@ pub async fn run() -> Result<Option<String>> {
270
256
  config.disabled_tools = disabled;
271
257
  save_config(&config)?;
272
258
 
273
- println!("\x1b[32mSuccessfully updated tool configurations!\x1b[0m\n");
259
+ println!("\x1b[32mSuccessfully updated agent tool configurations!\x1b[0m\n");
274
260
 
275
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
276
- println!("\x1b[32m Choose where to run Mint AI Agent\x1b[0m");
277
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
278
- println!("Select the environment you want to launch:");
279
- println!(" \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Enter: Confirm]\x1b[0m");
261
+ let all_native = native_plugins();
262
+ let allowed_plugins: std::collections::HashSet<String> = config
263
+ .extra
264
+ .get("allowedNativePlugins")
265
+ .and_then(|v| v.as_array())
266
+ .map(|arr| {
267
+ arr.iter()
268
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
269
+ .collect()
270
+ })
271
+ .unwrap_or_else(|| {
272
+ vec![
273
+ "dev_tools".to_string(),
274
+ "system_metrics".to_string(),
275
+ "github".to_string(),
276
+ ]
277
+ .into_iter()
278
+ .collect()
279
+ });
280
+
281
+ let mut native_options: Vec<ToolOption> = all_native
282
+ .iter()
283
+ .map(|p| {
284
+ let has_all_permission = allowed_plugins.contains("*");
285
+ let is_enabled = has_all_permission || allowed_plugins.contains(p.name);
286
+ ToolOption {
287
+ name: p.name,
288
+ key: p.name,
289
+ enabled: is_enabled,
290
+ }
291
+ })
292
+ .collect();
293
+
294
+ let mut native_cursor = 0;
295
+ redraw_phase_2(&native_options, native_cursor);
296
+ enable_raw_mode()?;
297
+
298
+ loop {
299
+ match event::poll(std::time::Duration::from_millis(100)) {
300
+ Ok(true) => {
301
+ if let Event::Key(key_event) = event::read()? {
302
+ if key_event.kind == event::KeyEventKind::Press {
303
+ let is_ctrl_c = matches!(key_event.code, KeyCode::Char('c'))
304
+ && key_event
305
+ .modifiers
306
+ .contains(crossterm::event::KeyModifiers::CONTROL);
307
+ if is_ctrl_c {
308
+ disable_raw_mode()?;
309
+ println!("\n\x1b[31mSetup cancelled.\x1b[0m");
310
+ return Ok(None);
311
+ }
312
+
313
+ match key_event.code {
314
+ KeyCode::Up => {
315
+ if native_cursor > 0 {
316
+ native_cursor -= 1;
317
+ } else {
318
+ native_cursor = native_options.len() - 1;
319
+ }
320
+ disable_raw_mode()?;
321
+ redraw_phase_2(&native_options, native_cursor);
322
+ enable_raw_mode()?;
323
+ }
324
+ KeyCode::Down => {
325
+ if native_cursor < native_options.len() - 1 {
326
+ native_cursor += 1;
327
+ } else {
328
+ native_cursor = 0;
329
+ }
330
+ disable_raw_mode()?;
331
+ redraw_phase_2(&native_options, native_cursor);
332
+ enable_raw_mode()?;
333
+ }
334
+ KeyCode::Char(' ') => {
335
+ native_options[native_cursor].enabled =
336
+ !native_options[native_cursor].enabled;
337
+ disable_raw_mode()?;
338
+ redraw_phase_2(&native_options, native_cursor);
339
+ enable_raw_mode()?;
340
+ }
341
+ KeyCode::Char('a') => {
342
+ for opt in &mut native_options {
343
+ opt.enabled = true;
344
+ }
345
+ disable_raw_mode()?;
346
+ redraw_phase_2(&native_options, native_cursor);
347
+ enable_raw_mode()?;
348
+ }
349
+ KeyCode::Char('i') => {
350
+ for opt in &mut native_options {
351
+ opt.enabled = !opt.enabled;
352
+ }
353
+ disable_raw_mode()?;
354
+ redraw_phase_2(&native_options, native_cursor);
355
+ enable_raw_mode()?;
356
+ }
357
+ KeyCode::Enter => {
358
+ break;
359
+ }
360
+ _ => {}
361
+ }
362
+ }
363
+ }
364
+ }
365
+ Ok(false) => {}
366
+ Err(_) => {
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ disable_raw_mode()?;
280
372
  println!();
281
373
 
374
+ let enabled_natives: Vec<serde_json::Value> = native_options
375
+ .iter()
376
+ .filter(|o| o.enabled)
377
+ .map(|o| serde_json::Value::String(o.key.to_string()))
378
+ .collect();
379
+
380
+ config.extra.insert(
381
+ "allowedNativePlugins".to_string(),
382
+ serde_json::Value::Array(enabled_natives),
383
+ );
384
+ save_config(&config)?;
385
+
386
+ println!("\x1b[32mSuccessfully updated native plugins configurations!\x1b[0m\n");
387
+
282
388
  let run_options = vec![
283
389
  ToolOption {
284
390
  name: "1. CLI (Interactive Terminal Assistant)",
@@ -298,7 +404,7 @@ pub async fn run() -> Result<Option<String>> {
298
404
  ];
299
405
 
300
406
  let mut run_cursor = 0;
301
- print_run_options(&run_options, run_cursor);
407
+ redraw_phase_3(&run_options, run_cursor);
302
408
  enable_raw_mode()?;
303
409
 
304
410
  loop {
@@ -324,8 +430,7 @@ pub async fn run() -> Result<Option<String>> {
324
430
  run_cursor = run_options.len() - 1;
325
431
  }
326
432
  disable_raw_mode()?;
327
- print!("\x1b[{}A\x1b[J", run_options.len());
328
- print_run_options(&run_options, run_cursor);
433
+ redraw_phase_3(&run_options, run_cursor);
329
434
  enable_raw_mode()?;
330
435
  }
331
436
  KeyCode::Down => {
@@ -335,8 +440,7 @@ pub async fn run() -> Result<Option<String>> {
335
440
  run_cursor = 0;
336
441
  }
337
442
  disable_raw_mode()?;
338
- print!("\x1b[{}A\x1b[J", run_options.len());
339
- print_run_options(&run_options, run_cursor);
443
+ redraw_phase_3(&run_options, run_cursor);
340
444
  enable_raw_mode()?;
341
445
  }
342
446
  KeyCode::Enter => {
@@ -388,3 +492,40 @@ fn print_run_options(options: &[ToolOption], cursor: usize) {
388
492
  }
389
493
  let _ = io::stdout().flush();
390
494
  }
495
+
496
+ fn redraw_phase_1(options: &[ToolOption], cursor: usize) {
497
+ print!("\x1b[2J\x1b[1;1H");
498
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
499
+ println!("\x1b[32m Mint CLI Tool Manager Wizard\x1b[0m");
500
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
501
+ println!("Configure which agent tools are enabled or disabled:");
502
+ println!(
503
+ " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
504
+ );
505
+ println!();
506
+ print_options(options, cursor);
507
+ }
508
+
509
+ fn redraw_phase_2(options: &[ToolOption], cursor: usize) {
510
+ print!("\x1b[2J\x1b[1;1H");
511
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
512
+ println!("\x1b[32m Configure Native Plugins Access\x1b[0m");
513
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
514
+ println!("Select which native plugins are allowed to run:");
515
+ println!(
516
+ " \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Space: Toggle | a: All | i: Invert | Enter: Confirm]\x1b[0m"
517
+ );
518
+ println!();
519
+ print_options(options, cursor);
520
+ }
521
+
522
+ fn redraw_phase_3(options: &[ToolOption], cursor: usize) {
523
+ print!("\x1b[2J\x1b[1;1H");
524
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
525
+ println!("\x1b[32m Choose where to run Mint AI Agent\x1b[0m");
526
+ println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
527
+ println!("Select the environment you want to launch:");
528
+ println!(" \x1b[90m[Keyboard Controls: ↑/↓: Navigate | Enter: Confirm]\x1b[0m");
529
+ println!();
530
+ print_run_options(options, cursor);
531
+ }
@@ -11,9 +11,10 @@ use tokio::net::TcpListener;
11
11
 
12
12
  use crate::{
13
13
  AgentProgress, ApprovalOutcome, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID,
14
- MemoryStore, MintConfig, config_path, create_folder, find_paths, list_saved_pictures,
15
- load_config, orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
16
- orchestrate_chat_with_fallback, save_chat_images, save_config, weather,
14
+ ImageGenRequest, MemoryStore, MintConfig, config_path, create_folder, find_paths,
15
+ generate_images, list_saved_pictures, load_config, orchestrate_agent_loop,
16
+ orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback, save_chat_images,
17
+ save_config, weather,
17
18
  };
18
19
 
19
20
  const MAX_API_REQUEST_BYTES: usize = 32 * 1024 * 1024;
@@ -271,6 +272,45 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
271
272
  .await;
272
273
  }
273
274
  }
275
+ ("POST", "/api/interactions/agent-activity") => {
276
+ #[derive(Deserialize)]
277
+ #[serde(rename_all = "camelCase")]
278
+ struct SaveAgentActivityRequest {
279
+ interaction_id: i64,
280
+ activity: Vec<AgentProgress>,
281
+ }
282
+
283
+ if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body) {
284
+ if let Ok(memory) = MemoryStore::open_default() {
285
+ match serde_json::to_string(&payload.activity) {
286
+ Ok(activity_json) => {
287
+ if memory
288
+ .set_interaction_agent_activity_json(
289
+ payload.interaction_id,
290
+ &activity_json,
291
+ )
292
+ .is_ok()
293
+ {
294
+ send_json_response(
295
+ socket,
296
+ "200 OK",
297
+ "{\"status\":\"ok\"}",
298
+ )
299
+ .await;
300
+ return;
301
+ }
302
+ }
303
+ Err(_) => {}
304
+ }
305
+ }
306
+ }
307
+ send_json_response(
308
+ socket,
309
+ "400 Bad Request",
310
+ "{\"status\":\"invalid agent activity payload\"}",
311
+ )
312
+ .await;
313
+ }
274
314
  ("GET", "/api/pictures") => match list_saved_pictures() {
275
315
  Ok(mut pictures) => {
276
316
  for picture in &mut pictures {
@@ -663,6 +703,110 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
663
703
  )
664
704
  .await;
665
705
  }
706
+ ("POST", "/api/image-generate") => {
707
+ #[derive(serde::Deserialize)]
708
+ #[serde(rename_all = "camelCase")]
709
+ struct ImageGenApiRequest {
710
+ prompt: String,
711
+ #[serde(default)]
712
+ negative_prompt: Option<String>,
713
+ #[serde(default)]
714
+ aspect_ratio: Option<String>,
715
+ #[serde(default)]
716
+ num_images: Option<u8>,
717
+ #[serde(default)]
718
+ model: Option<String>,
719
+ /// Which image provider to use (overrides config.image_gen_provider).
720
+ #[serde(default)]
721
+ provider: Option<String>,
722
+ }
723
+
724
+ if let Ok(req) = serde_json::from_str::<ImageGenApiRequest>(body) {
725
+ let config = load_config().unwrap_or_default();
726
+ let gen_request = ImageGenRequest {
727
+ prompt: req.prompt.clone(),
728
+ negative_prompt: req.negative_prompt,
729
+ aspect_ratio: req.aspect_ratio,
730
+ num_images: req.num_images,
731
+ model: req.model,
732
+ provider: req.provider,
733
+ };
734
+ match generate_images(&config, &gen_request).await {
735
+ Ok(result) => {
736
+ let data_uris: Vec<String> = result
737
+ .images
738
+ .iter()
739
+ .map(|img| img.data_uri.clone())
740
+ .collect();
741
+ let mut saved = save_chat_images(
742
+ data_uris,
743
+ Some(result.provider.clone()),
744
+ Some(req.prompt.clone()),
745
+ )
746
+ .unwrap_or_default();
747
+ for picture in &mut saved {
748
+ picture.url =
749
+ Some(format!("/api/pictures/{}", picture.filename));
750
+ picture.thumbnail_url =
751
+ Some(format!("/api/pictures/{}", picture.filename));
752
+ }
753
+ let response = json!({
754
+ "images": saved,
755
+ "model": result.model,
756
+ "provider": result.provider,
757
+ "prompt": result.prompt,
758
+ "description": result.description
759
+ });
760
+ send_json_response(socket, "200 OK", &response.to_string()).await;
761
+ }
762
+ Err(e) => {
763
+ let err = json!({ "error": e.to_string() });
764
+ send_json_response(
765
+ socket,
766
+ "500 Internal Server Error",
767
+ &err.to_string(),
768
+ )
769
+ .await;
770
+ }
771
+ }
772
+ } else {
773
+ send_json_response(
774
+ socket,
775
+ "400 Bad Request",
776
+ "{\"error\":\"invalid image generation request body\"}",
777
+ )
778
+ .await;
779
+ }
780
+ }
781
+ ("GET", "/api/image-gen/providers") => {
782
+ let config = load_config().unwrap_or_default();
783
+ let mut available: Vec<String> = Vec::new();
784
+ if !config.api_key.trim().is_empty() {
785
+ available.push("nanobanana".into());
786
+ }
787
+ if !config.openai_api_key.trim().is_empty() {
788
+ available.push("dalle".into());
789
+ }
790
+ if !config.stability_api_key.trim().is_empty() {
791
+ available.push("stability".into());
792
+ }
793
+ if !config.ideogram_api_key.trim().is_empty() {
794
+ available.push("ideogram".into());
795
+ }
796
+ if !config.replicate_api_key.trim().is_empty() {
797
+ available.push("replicate".into());
798
+ }
799
+ let active = if available.contains(&config.image_gen_provider) {
800
+ config.image_gen_provider.clone()
801
+ } else {
802
+ available
803
+ .first()
804
+ .cloned()
805
+ .unwrap_or_else(|| "nanobanana".into())
806
+ };
807
+ let response = json!({ "active": active, "available": available });
808
+ send_json_response(socket, "200 OK", &response.to_string()).await;
809
+ }
666
810
  _ => {
667
811
  send_json_response(socket, "404 Not Found", "{\"error\":\"Not Found\"}").await;
668
812
  }
@@ -60,6 +60,16 @@ pub struct MintConfig {
60
60
  pub local_model_name: String,
61
61
  pub ollama_host: String,
62
62
  pub ollama_model: String,
63
+ pub nanobanana_model: String,
64
+ /// Active image-generation provider: "nanobanana" | "dalle" | "stability" | "ideogram" | "replicate"
65
+ pub image_gen_provider: String,
66
+ pub dalle_model: String,
67
+ pub stability_api_key: String,
68
+ pub stability_model: String,
69
+ pub ideogram_api_key: String,
70
+ pub ideogram_model: String,
71
+ pub replicate_api_key: String,
72
+ pub replicate_model: String,
63
73
  pub show_desktop_widget: bool,
64
74
  pub safety_enabled: bool,
65
75
  pub sandbox_mode: String,
@@ -89,7 +99,7 @@ impl Default for MintConfig {
89
99
  ];
90
100
  Self {
91
101
  theme: "dark".into(),
92
- accent_color: "#8b5cf6".into(),
102
+ accent_color: "#10b981".into(),
93
103
  language: "th-TH".into(),
94
104
  assistant_mode: "chat".into(),
95
105
  ai_provider: "gemini".into(),
@@ -109,6 +119,15 @@ impl Default for MintConfig {
109
119
  local_model_name: "local-model".into(),
110
120
  ollama_host: String::new(),
111
121
  ollama_model: "llama3:latest".into(),
122
+ nanobanana_model: "gemini-2.5-flash-image".into(),
123
+ image_gen_provider: "nanobanana".into(),
124
+ dalle_model: "dall-e-3".into(),
125
+ stability_api_key: String::new(),
126
+ stability_model: "sd3.5-large".into(),
127
+ ideogram_api_key: String::new(),
128
+ ideogram_model: "V_3".into(),
129
+ replicate_api_key: String::new(),
130
+ replicate_model: "black-forest-labs/flux-1.1-pro".into(),
112
131
  show_desktop_widget: true,
113
132
  safety_enabled: true,
114
133
  sandbox_mode: "prefer".into(),
@@ -291,7 +310,7 @@ fn runtime_extra_defaults() -> BTreeMap<String, Value> {
291
310
  "notionPageId": "",
292
311
  "notionTitleProperty": "Name",
293
312
  "allowedShellModes": ["readOnly", "test", "mutating", "network"],
294
- "allowedNativePlugins": ["dev_tools", "system_metrics"],
313
+ "allowedNativePlugins": ["dev_tools", "system_metrics", "github"],
295
314
  "allowedMcpTools": {},
296
315
  "mcpServers": {}
297
316
  }))
@@ -361,7 +380,7 @@ mod tests {
361
380
  );
362
381
  assert_eq!(
363
382
  config.extra["allowedNativePlugins"],
364
- serde_json::json!(["dev_tools", "system_metrics"])
383
+ serde_json::json!(["dev_tools", "system_metrics", "github"])
365
384
  );
366
385
  assert_eq!(config.extra["allowedMcpTools"], serde_json::json!({}));
367
386
  }