@pheem49/mint 1.8.0 → 1.8.2

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 (200) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +20 -6
  5. package/Release_Note.md +129 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/bin/mint +0 -0
  8. package/crates/mint-cli/Cargo.toml +1 -0
  9. package/crates/mint-cli/src/agent.rs +242 -54
  10. package/crates/mint-cli/src/main.rs +397 -74
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +64 -2
  13. package/crates/mint-core/src/browser.rs +456 -0
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +3 -0
  16. package/crates/mint-core/src/config.rs +130 -1
  17. package/crates/mint-core/src/lib.rs +19 -0
  18. package/crates/mint-core/src/orchestration.rs +317 -12
  19. package/crates/mint-core/src/skills.rs +113 -4
  20. package/crates/mint-core/src/tts.rs +3 -1
  21. package/crates/mint-core/src/workflows.rs +1 -22
  22. package/flatpak/com.pheem49.mint.desktop +9 -0
  23. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  24. package/flatpak/com.pheem49.mint.yaml +52 -0
  25. package/package.json +1 -1
  26. package/shared-modules/.github/workflows/ci.yml +120 -0
  27. package/shared-modules/.github/workflows/update.yaml +28 -0
  28. package/shared-modules/CODEOWNERS +23 -0
  29. package/shared-modules/README.md +79 -0
  30. package/shared-modules/SDL/README.md +3 -0
  31. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  32. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  33. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  34. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  35. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  36. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  37. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  38. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  39. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  40. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  41. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  42. package/shared-modules/SDL/sdl12-compat.json +22 -0
  43. package/shared-modules/SDL2/SDL2_image.json +27 -0
  44. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  45. package/shared-modules/cld2/CMakeLists.txt +157 -0
  46. package/shared-modules/cld2/cld2.json +24 -0
  47. package/shared-modules/clutter/clutter.json +72 -0
  48. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  49. package/shared-modules/glew/glew.json +36 -0
  50. package/shared-modules/glu/glu-9.json +12 -0
  51. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  52. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  53. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  54. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  55. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  56. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  57. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  58. package/shared-modules/gtk2/gtk2.json +120 -0
  59. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  60. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  61. package/shared-modules/gtk2/readme.md +10 -0
  62. package/shared-modules/intltool/intltool-0.51.json +20 -0
  63. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  64. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  65. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  66. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  67. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  68. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  69. package/shared-modules/lame/lame-3.99.5.json +32 -0
  70. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  71. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  72. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  73. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  74. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  75. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  76. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  77. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  78. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  79. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  80. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  81. package/shared-modules/libcanberra/libcanberra.json +44 -0
  82. package/shared-modules/libmad/libmad.json +19 -0
  83. package/shared-modules/libsecret/libsecret.json +23 -0
  84. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  85. package/shared-modules/libusb/libusb.json +27 -0
  86. package/shared-modules/linux-audio/dssi.json +31 -0
  87. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  88. package/shared-modules/linux-audio/fftw3f.json +42 -0
  89. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  90. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  91. package/shared-modules/linux-audio/ladspa.json +26 -0
  92. package/shared-modules/linux-audio/lash.json +35 -0
  93. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  94. package/shared-modules/linux-audio/liblo-static.json +22 -0
  95. package/shared-modules/linux-audio/liblo.json +17 -0
  96. package/shared-modules/linux-audio/lilv-static.json +94 -0
  97. package/shared-modules/linux-audio/lilv.json +82 -0
  98. package/shared-modules/linux-audio/lrdf.json +57 -0
  99. package/shared-modules/linux-audio/lv2.json +21 -0
  100. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  101. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  102. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  103. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  104. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  105. package/shared-modules/linux-audio/stk.json +28 -0
  106. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  107. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  108. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  109. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  110. package/shared-modules/lua5.3/lua.pc.in +8 -0
  111. package/shared-modules/lua5.3/luaconf.patch +47 -0
  112. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  113. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  114. package/shared-modules/lua5.4/lua.pc.in +8 -0
  115. package/shared-modules/lua5.4/luaconf.patch +34 -0
  116. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  117. package/shared-modules/luajit/luajit.json +30 -0
  118. package/shared-modules/lzo/lzo.json +27 -0
  119. package/shared-modules/mac/mac.json +18 -0
  120. package/shared-modules/physfs/physfs.json +26 -0
  121. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  122. package/shared-modules/pygame/cython-fixes.patch +34 -0
  123. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  124. package/shared-modules/pygame/glib.patch +25 -0
  125. package/shared-modules/pygame/pygame1-python2.json +157 -0
  126. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  127. package/shared-modules/pygtk/pygtk.json +117 -0
  128. package/shared-modules/pygtk/pygtk.patch +39 -0
  129. package/shared-modules/python2.7/python-2.7.json +48 -0
  130. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  131. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  132. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  133. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  134. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  135. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  136. package/shared-modules/update-checker.sh +50 -0
  137. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  138. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  139. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  140. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  141. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  142. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  143. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  144. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  145. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  146. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  147. package/shared-modules/vorbisgain/README.md +23 -0
  148. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  149. package/src/renderer/shared/agentProgress.ts +145 -0
  150. package/src/renderer/shared/calculator.ts +52 -0
  151. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  152. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  153. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  154. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  155. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  156. package/src/renderer/shared/constants/models.ts +91 -0
  157. package/src/renderer/shared/platform.ts +82 -0
  158. package/src/renderer/shared/types.ts +140 -0
  159. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  160. package/src/renderer/shared/utils/approval.ts +1 -0
  161. package/src/renderer/shared/utils/approval.tsx +64 -0
  162. package/src/renderer/shared/utils/markdown.tsx +347 -0
  163. package/src/renderer/shared/utils/providers.ts +36 -0
  164. package/src/renderer/shared/utils/speech.ts +216 -0
  165. package/src/renderer/shared/utils/ui.ts +147 -0
  166. package/src/renderer/src/agentProgress.ts +11 -39
  167. package/src/renderer/src/calculator.ts +3 -46
  168. package/src/renderer/src/components/ChatPanel.tsx +230 -1202
  169. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  170. package/src/renderer/src/components/MintDashboard.tsx +63 -171
  171. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  172. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  173. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  174. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  175. package/src/renderer/src/components/SettingsWindow.tsx +54 -12
  176. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  177. package/src/renderer/src/css/image-studio.css +6 -0
  178. package/src/renderer/src/css/styles.css +148 -11
  179. package/src/renderer/src/index.css +12 -1
  180. package/src/renderer/src/tauri.ts +169 -143
  181. package/src/renderer/src-web/agentProgress.ts +11 -39
  182. package/src/renderer/src-web/calculator.ts +3 -46
  183. package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
  184. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  185. package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
  186. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  187. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  188. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  189. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  190. package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
  191. package/src/renderer/src-web/css/image-studio.css +6 -0
  192. package/src/renderer/src-web/css/styles.css +184 -12
  193. package/src/renderer/src-web/index.css +12 -1
  194. package/src/renderer/src-web/tauri.ts +204 -104
  195. package/src-tauri/src/browser.rs +1 -141
  196. package/src-tauri/src/headless.rs +3 -2
  197. package/src-tauri/src/lib.rs +254 -59
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +57 -4
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -110,6 +110,8 @@ enum Command {
110
110
  },
111
111
  /// Run one queued or supplied task through the native CLI agent.
112
112
  Agent { task: Option<String> },
113
+ /// Start the browser automation environment and enable browser actions.
114
+ Auto,
113
115
  /// Launch the web UI and local API server.
114
116
  Web,
115
117
  /// Start only the local API server.
@@ -453,6 +455,21 @@ enum MemoryCommand {
453
455
  },
454
456
  }
455
457
 
458
+ fn print_mcp_servers(servers: &std::collections::BTreeMap<String, mint_core::mcp::McpServer>) {
459
+ if servers.is_empty() {
460
+ println!("{DIM}(No MCP servers configured.){RESET}\n");
461
+ return;
462
+ }
463
+ for (name, srv) in servers {
464
+ let args_str = srv.args.join(" ");
465
+ println!(
466
+ " {BLUE}●{RESET} {name} {DIM}({} {}){RESET}",
467
+ srv.command, args_str
468
+ );
469
+ }
470
+ println!();
471
+ }
472
+
456
473
  #[tokio::main]
457
474
  async fn main() -> Result<()> {
458
475
  match Cli::parse().command {
@@ -523,6 +540,99 @@ async fn main() -> Result<()> {
523
540
  Command::Agent { task } => {
524
541
  run_cli_agent_task(task).await?;
525
542
  }
543
+ Command::Auto => {
544
+ println!("🚀 Starting Mint Browser Automation Environment...");
545
+ let config = mint_core::load_config()?;
546
+ mint_core::spawn_automation_browser(&config)
547
+ .await
548
+ .map_err(anyhow::Error::msg)?;
549
+
550
+ // Enable the browser tools if they are disabled
551
+ let mut config_mut = config.clone();
552
+ let mut changed = false;
553
+ for tool in &[
554
+ "browser_open",
555
+ "browser_click",
556
+ "browser_type",
557
+ "browser_read",
558
+ ] {
559
+ if config_mut.disabled_tools.contains(&tool.to_string()) {
560
+ config_mut.disabled_tools.retain(|x| x != *tool);
561
+ changed = true;
562
+ }
563
+ }
564
+ if changed {
565
+ mint_core::save_config(&config_mut)?;
566
+ println!(
567
+ "✅ Enabled browser automation tools in config: browser_open, browser_click, browser_type, browser_read"
568
+ );
569
+ }
570
+
571
+ println!(
572
+ "🌐 Isolated browser running with remote debugging on http://127.0.0.1:9222"
573
+ );
574
+ println!(
575
+ "💬 Keep this terminal open while you want Mint to automate browser tasks."
576
+ );
577
+ println!("Press Ctrl+C to terminate the automation browser session.");
578
+ println!("----------------------------------------------------------------------");
579
+
580
+ // Start tailing the log file in a background task
581
+ let log_dir = dirs::config_dir()
582
+ .unwrap_or_else(|| std::env::temp_dir())
583
+ .join("mint");
584
+ let log_file = log_dir.join("browser-automation.log");
585
+
586
+ // Clear existing log file on startup
587
+ let _ = std::fs::remove_file(&log_file);
588
+
589
+ let log_file_clone = log_file.clone();
590
+ tokio::spawn(async move {
591
+ use std::io::{BufRead, BufReader, Seek, SeekFrom};
592
+ let mut file_pos = 0;
593
+ loop {
594
+ if log_file_clone.exists() {
595
+ if let Ok(mut file) = std::fs::File::open(&log_file_clone) {
596
+ if let Ok(_) = file.seek(SeekFrom::Start(file_pos)) {
597
+ let reader = BufReader::new(file);
598
+ for line in reader.lines() {
599
+ if let Ok(line_str) = line {
600
+ if line_str.contains("[NAVIGATE]")
601
+ || line_str.contains("[NAVIGATE_SUCCESS]")
602
+ {
603
+ println!("🌐 {line_str}");
604
+ } else if line_str.contains("[CLICK]")
605
+ || line_str.contains("[CLICK_SUCCESS]")
606
+ {
607
+ println!("🖱️ {line_str}");
608
+ } else if line_str.contains("[TYPE]")
609
+ || line_str.contains("[TYPE_SUCCESS]")
610
+ {
611
+ println!("⌨️ {line_str}");
612
+ } else if line_str.contains("[READ]")
613
+ || line_str.contains("[READ_SUCCESS]")
614
+ {
615
+ println!("📖 {line_str}");
616
+ } else if line_str.contains("_ERROR]") {
617
+ println!("❌ {line_str}");
618
+ } else {
619
+ println!("📝 {line_str}");
620
+ }
621
+ }
622
+ }
623
+ if let Ok(pos) = log_file_clone.metadata().map(|m| m.len()) {
624
+ file_pos = pos;
625
+ }
626
+ }
627
+ }
628
+ }
629
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
630
+ }
631
+ });
632
+
633
+ tokio::signal::ctrl_c().await.ok();
634
+ println!("\n👋 Terminating Mint Browser Automation Environment...");
635
+ }
526
636
  Command::Web => {
527
637
  launch_mint_target("web".into()).await?;
528
638
  }
@@ -539,7 +649,10 @@ async fn main() -> Result<()> {
539
649
  mcp::add(&name, &command, args, env)?;
540
650
  println!("Added MCP server: {name}");
541
651
  }
542
- McpCommand::List => println!("{}", serde_json::to_string_pretty(&mcp::list()?)?),
652
+ McpCommand::List => {
653
+ println!("\n{BLUE}MCP servers:{RESET}");
654
+ print_mcp_servers(&mcp::list()?);
655
+ }
543
656
  McpCommand::Remove { name } => {
544
657
  println!(
545
658
  "{}",
@@ -585,10 +698,55 @@ async fn main() -> Result<()> {
585
698
  Command::Learn { path, list, delete } => {
586
699
  let memory = MemoryStore::open_default()?;
587
700
  if list {
588
- println!(
589
- "{}",
590
- serde_json::to_string_pretty(&memory.learned_skills(100)?)?
591
- );
701
+ let mut skills = memory.learned_skills(100)?;
702
+
703
+ if let Some(home) = dirs::home_dir() {
704
+ let global_skills_path =
705
+ home.join(".config").join("mint").join("mint-skills");
706
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
707
+ }
708
+ if let Ok(root) = std::env::current_dir() {
709
+ if let Ok(root) = root.canonicalize() {
710
+ let workspace_skills_path1 = root.join(".agents").join("skills");
711
+ mint_core::skills::load_skills_from_dir(
712
+ &workspace_skills_path1,
713
+ &mut skills,
714
+ );
715
+
716
+ let workspace_skills_path2 = root.join("skills");
717
+ mint_core::skills::load_skills_from_dir(
718
+ &workspace_skills_path2,
719
+ &mut skills,
720
+ );
721
+ }
722
+ }
723
+
724
+ let mut unique_skills = std::collections::BTreeMap::new();
725
+ for skill in skills {
726
+ let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
727
+ "Global"
728
+ } else if skill.source_path.contains("/skills")
729
+ || skill.source_path.contains("/.agents/skills")
730
+ {
731
+ "Workspace"
732
+ } else {
733
+ "Taught"
734
+ };
735
+ unique_skills.insert(skill.name.clone(), (skill, loc));
736
+ }
737
+
738
+ if unique_skills.is_empty() {
739
+ println!("No learned skills found.");
740
+ } else {
741
+ println!("Learned AI Skills:");
742
+ for (name, (skill, loc)) in &unique_skills {
743
+ if *loc == "Taught" {
744
+ println!(" ● [{}] {}", loc, name);
745
+ } else {
746
+ println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
747
+ }
748
+ }
749
+ }
592
750
  } else if let Some(identifier) = delete {
593
751
  println!("{}", memory.delete_learned_skill(&identifier)?);
594
752
  } else if let Some(path) = path {
@@ -645,6 +803,7 @@ async fn main() -> Result<()> {
645
803
  audio_data_uri: None,
646
804
  document_attachment: None,
647
805
  workspace_path: None,
806
+ agent_id: None,
648
807
  },
649
808
  )
650
809
  .await?;
@@ -1315,6 +1474,9 @@ impl CliStreamFormatter {
1315
1474
  print_fn(ch);
1316
1475
  }
1317
1476
  print_fn('\n');
1477
+ for ch in " ".chars() {
1478
+ print_fn(ch);
1479
+ }
1318
1480
  } else {
1319
1481
  if is_table_line(&line) {
1320
1482
  self.table_buffer.push(line);
@@ -1327,6 +1489,9 @@ impl CliStreamFormatter {
1327
1489
  print_fn(ch);
1328
1490
  }
1329
1491
  print_fn('\n');
1492
+ for ch in " ".chars() {
1493
+ print_fn(ch);
1494
+ }
1330
1495
  }
1331
1496
  }
1332
1497
  }
@@ -1597,8 +1762,12 @@ async fn handle_slash_command(
1597
1762
  ("/mcp list", "List configured MCP servers"),
1598
1763
  ("/mcp allow <server> <tool>", "Allow an MCP tool"),
1599
1764
  ("/stats", "Show session statistics"),
1600
- ("/exit | /quit", "Exit Mint"),
1765
+ ("/exit", "Exit Mint"),
1601
1766
  ("/code <task>", "Run in code-agent mode"),
1767
+ (
1768
+ "/multi-agent [on|off]",
1769
+ "Show or toggle Multi-Agent Collaboration system",
1770
+ ),
1602
1771
  ];
1603
1772
  for (cmd_name, desc) in &commands {
1604
1773
  println!(" {MINT}{:<30}{RESET} {DIM}{}{RESET}", cmd_name, desc);
@@ -1639,13 +1808,77 @@ async fn handle_slash_command(
1639
1808
  println!();
1640
1809
  } else {
1641
1810
  session.config.ai_provider = rest.to_owned();
1811
+ match mint_core::save_config(&session.config) {
1812
+ Ok(()) => {
1813
+ let active_model = active_model(&session.config.ai_provider, &session.config);
1814
+ let display_name = format!("Changed model to {} • {}", session.config.ai_provider, active_model);
1815
+
1816
+ // Save system event interaction in memory
1817
+ if let Ok(memory) = MemoryStore::open_default() {
1818
+ let _ = memory.add_interaction_for_chat_with_fallback(
1819
+ CHAT_CLI_ID,
1820
+ &display_name,
1821
+ "",
1822
+ "system",
1823
+ "provider_change",
1824
+ None,
1825
+ );
1826
+ }
1827
+
1828
+ println!(
1829
+ "\n{DIM}───{RESET} {MINT}{}{RESET} {DIM}───{RESET}\n",
1830
+ display_name
1831
+ );
1832
+ }
1833
+ Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1834
+ }
1835
+ }
1836
+ Some(SlashResult::Handled)
1837
+ }
1838
+
1839
+ "/multi-agent" => {
1840
+ if rest == "on" || rest == "off" {
1841
+ session.config.enable_agent_collaboration = rest == "on";
1642
1842
  match mint_core::save_config(&session.config) {
1643
1843
  Ok(()) => println!(
1644
- "{DIM}Switched to provider: {}{RESET}\n",
1645
- session.config.ai_provider
1844
+ "{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
1845
+ if session.config.enable_agent_collaboration { "Enabled" } else { "Disabled" }
1646
1846
  ),
1647
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1847
+ Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1648
1848
  }
1849
+ } else if !rest.is_empty() {
1850
+ println!("{WARN}Usage: /multi-agent [on|off]{RESET}\n");
1851
+ } else {
1852
+ println!("\n{BLUE}Multi-Agent System Settings:{RESET}");
1853
+ let collab_status = if session.config.enable_agent_collaboration {
1854
+ format!("{MINT}Enabled (on){RESET}")
1855
+ } else {
1856
+ format!("{DIM}Disabled (off){RESET}")
1857
+ };
1858
+ println!(" Global Collaboration: {collab_status}");
1859
+ println!("\n{BLUE}Configured Agents:{RESET}");
1860
+ if session.config.agents.is_empty() {
1861
+ println!(" No agents configured.");
1862
+ } else {
1863
+ for agent in &session.config.agents {
1864
+ let status_label = if agent.enabled {
1865
+ format!("{MINT}[Enabled]{RESET}")
1866
+ } else {
1867
+ format!("{DIM}[Disabled]{RESET}")
1868
+ };
1869
+ println!(
1870
+ " {:<15} {} Provider: {:<12} Model: {}",
1871
+ agent.name, status_label, agent.provider, agent.model
1872
+ );
1873
+ let truncated_instruction = if agent.system_instruction.len() > 60 {
1874
+ format!("{}...", &agent.system_instruction[..60].replace('\n', " "))
1875
+ } else {
1876
+ agent.system_instruction.replace('\n', " ")
1877
+ };
1878
+ println!(" {DIM}Instruction: {truncated_instruction}{RESET}");
1879
+ }
1880
+ }
1881
+ println!();
1649
1882
  }
1650
1883
  Some(SlashResult::Handled)
1651
1884
  }
@@ -1792,7 +2025,56 @@ async fn handle_slash_command(
1792
2025
 
1793
2026
  "/learn" => {
1794
2027
  if rest.is_empty() {
1795
- println!("{WARN}/learn usage: /learn <path>{RESET}\n");
2028
+ let mut skills = match MemoryStore::open_default() {
2029
+ Ok(m) => match m.learned_skills(100) {
2030
+ Ok(s) => s,
2031
+ Err(e) => {
2032
+ println!("{ERROR}Error loading custom skills: {e}{RESET}\n");
2033
+ return Some(SlashResult::Handled);
2034
+ }
2035
+ },
2036
+ Err(e) => {
2037
+ println!("{ERROR}Memory error: {e}{RESET}\n");
2038
+ return Some(SlashResult::Handled);
2039
+ }
2040
+ };
2041
+
2042
+ if let Some(home) = dirs::home_dir() {
2043
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
2044
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
2045
+ }
2046
+ let workspace_skills_path1 = session.current_dir.join(".agents").join("skills");
2047
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
2048
+ let workspace_skills_path2 = session.current_dir.join("skills");
2049
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
2050
+
2051
+ let mut unique_skills = std::collections::BTreeMap::new();
2052
+ for skill in skills {
2053
+ let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
2054
+ "Global"
2055
+ } else if skill.source_path.contains("/skills")
2056
+ || skill.source_path.contains("/.agents/skills")
2057
+ {
2058
+ "Workspace"
2059
+ } else {
2060
+ "Taught"
2061
+ };
2062
+ unique_skills.insert(skill.name.clone(), (skill, loc));
2063
+ }
2064
+
2065
+ if unique_skills.is_empty() {
2066
+ println!("No learned skills found. Use '/learn <path>' to add one.\n");
2067
+ } else {
2068
+ println!("Learned AI Skills:");
2069
+ for (name, (skill, loc)) in &unique_skills {
2070
+ if *loc == "Taught" {
2071
+ println!(" ● [{}] {}", loc, name);
2072
+ } else {
2073
+ println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
2074
+ }
2075
+ }
2076
+ println!();
2077
+ }
1796
2078
  } else {
1797
2079
  let path = PathBuf::from(rest);
1798
2080
  let path = if path.is_absolute() {
@@ -1834,8 +2116,10 @@ async fn handle_slash_command(
1834
2116
  println!(
1835
2117
  " {DIM}[{}]{RESET} {BLUE}You:{RESET} {}",
1836
2118
  &item.created_at[..16.min(item.created_at.len())],
1837
- if item.user_text.len() > 80 {
1838
- format!("{}…", &item.user_text[..80])
2119
+ if item.user_text.chars().count() > 80 {
2120
+ let short: String =
2121
+ item.user_text.chars().take(80).collect();
2122
+ format!("{}…", short)
1839
2123
  } else {
1840
2124
  item.user_text.clone()
1841
2125
  }
@@ -1903,15 +2187,9 @@ async fn handle_slash_command(
1903
2187
  .unwrap_or((rest, ""));
1904
2188
  match subcmd {
1905
2189
  "list" | "" => match mcp::list() {
1906
- Ok(servers) if servers.is_empty() => {
1907
- println!("{DIM}No MCP servers configured.{RESET}\n");
1908
- }
1909
2190
  Ok(servers) => {
1910
2191
  println!("\n{BLUE}MCP servers:{RESET}");
1911
- match serde_json::to_string_pretty(&servers) {
1912
- Ok(json) => println!("{json}\n"),
1913
- Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
1914
- }
2192
+ print_mcp_servers(&servers);
1915
2193
  }
1916
2194
  Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
1917
2195
  },
@@ -2291,7 +2569,7 @@ async fn run_interactive_chat() -> Result<()> {
2291
2569
  .to_owned();
2292
2570
 
2293
2571
  println!();
2294
- print!("{MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
2572
+ print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
2295
2573
  let _ = io::stdout().flush();
2296
2574
 
2297
2575
  let mut system_instruction = format!(
@@ -2333,11 +2611,12 @@ async fn run_interactive_chat() -> Result<()> {
2333
2611
  audio_data_uri: None,
2334
2612
  document_attachment: None,
2335
2613
  workspace_path: None,
2614
+ agent_id: None,
2336
2615
  },
2337
2616
  |chunk| {
2338
2617
  if first_chunk {
2339
2618
  first_chunk = false;
2340
- print!("\r\x1b[2K{MINT}Mint:{RESET} ");
2619
+ print!("\r\x1b[2K {MINT}Mint:{RESET} ");
2341
2620
  }
2342
2621
  filter.process_chunk(&chunk, |text| {
2343
2622
  for c in text.chars() {
@@ -2426,51 +2705,69 @@ fn print_exit_message(session: &InteractiveSession) {
2426
2705
  }
2427
2706
 
2428
2707
  const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
2429
- ("/help", "Show help menu"),
2708
+ ("/cd", "Change active workspace directory"),
2709
+ ("/clear", "Clear conversation history"),
2710
+ ("/code", "Run in code-agent mode"),
2711
+ ("/exit", "Exit Mint CLI"),
2430
2712
  ("/fast", "Toggle fast mode (hide thinking traces)"),
2431
- ("/models", "List AI providers or switch active provider"),
2713
+ ("/help", "Show help menu"),
2714
+ ("/image", "Attach image from disk"),
2432
2715
  (
2433
2716
  "/image-provider",
2434
2717
  "List image gen providers or switch default provider",
2435
2718
  ),
2436
- ("/clear", "Clear conversation history"),
2437
- ("/cd", "Change active workspace directory"),
2438
- ("/image", "Attach image from disk"),
2439
- ("/paste", "Attach image from clipboard"),
2440
2719
  ("/learn", "Import persistent skill/instruction"),
2441
- ("/plugins", "List or generate plugins/skills"),
2442
- ("/memory", "Manage long-term memory store"),
2443
2720
  ("/mcp", "List configured MCP servers"),
2721
+ ("/memory", "Manage long-term memory store"),
2722
+ ("/models", "List AI providers or switch active provider"),
2723
+ ("/multi-agent", "Show or toggle Multi-Agent Collaboration system"),
2724
+ ("/paste", "Attach image from clipboard"),
2725
+ ("/plugins", "List or generate plugins/skills"),
2444
2726
  ("/stats", "Show session statistics"),
2445
- ("/exit", "Exit Mint CLI"),
2446
- ("/quit", "Exit Mint CLI"),
2447
- ("/code", "Run in code-agent mode"),
2448
2727
  ];
2449
2728
 
2450
2729
  fn draw_input_box(
2451
- input: &str,
2730
+ input_chars: &[char],
2731
+ cursor_pos: usize,
2452
2732
  placeholder: &str,
2453
2733
  model: &str,
2454
2734
  path_str: &str,
2455
2735
  tab_base_input: Option<&str>,
2456
2736
  tab_index: Option<usize>,
2457
- ) -> usize {
2737
+ ) -> (usize, usize) {
2458
2738
  let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2459
2739
  let width = term_width as usize;
2460
2740
  let prefix = "› ";
2461
2741
  let input_width = width.saturating_sub(2);
2462
2742
  let content_max_len = input_width.saturating_sub(prefix.chars().count());
2463
2743
 
2464
- let display_str = if input.is_empty() {
2744
+ let mut start_idx = 0;
2745
+ if input_chars.len() > content_max_len {
2746
+ if cursor_pos >= content_max_len {
2747
+ start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
2748
+ if start_idx + content_max_len > input_chars.len() {
2749
+ start_idx = input_chars.len() - content_max_len;
2750
+ }
2751
+ }
2752
+ }
2753
+
2754
+ let slice_chars = if input_chars.len() > content_max_len {
2755
+ &input_chars[start_idx..std::cmp::min(start_idx + content_max_len, input_chars.len())]
2756
+ } else {
2757
+ input_chars
2758
+ };
2759
+ let displayed_input: String = slice_chars.iter().collect();
2760
+
2761
+ let display_str = if displayed_input.is_empty() {
2465
2762
  format!("{DIM}{}\x1b[39m", placeholder)
2466
2763
  } else {
2467
- format_placeholders(input)
2764
+ format_placeholders(&displayed_input)
2468
2765
  };
2469
2766
 
2470
- let visible_len = if input.is_empty() {
2767
+ let visible_len = if displayed_input.is_empty() {
2471
2768
  placeholder.chars().count()
2472
2769
  } else {
2473
- string_visual_width(input)
2770
+ string_visual_width(&displayed_input)
2474
2771
  };
2475
2772
 
2476
2773
  let pad_len = content_max_len.saturating_sub(visible_len);
@@ -2498,7 +2795,8 @@ fn draw_input_box(
2498
2795
  );
2499
2796
 
2500
2797
  // Compute and draw suggestions
2501
- let search_query = tab_base_input.unwrap_or(input);
2798
+ let raw_input: String = input_chars.iter().collect();
2799
+ let search_query = tab_base_input.unwrap_or(&raw_input);
2502
2800
  let mut match_count = 0;
2503
2801
  if search_query.starts_with('/') {
2504
2802
  let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
@@ -2507,11 +2805,18 @@ fn draw_input_box(
2507
2805
  .collect();
2508
2806
 
2509
2807
  if !matches.is_empty() {
2510
- match_count = matches.len();
2511
- println!();
2512
- println!(" {BLUE}Suggestions{RESET}");
2808
+ let total_pages = (matches.len() + 4) / 5;
2513
2809
  let highlight_idx = tab_index.map(|idx| idx % matches.len());
2514
- for (i, (cmd, desc)) in matches.iter().enumerate() {
2810
+ let selected_idx = highlight_idx.unwrap_or(0);
2811
+ let current_page = selected_idx / 5;
2812
+ let start_idx = current_page * 5;
2813
+ let end_idx = std::cmp::min(start_idx + 5, matches.len());
2814
+ match_count = end_idx - start_idx;
2815
+
2816
+ println!();
2817
+ println!(" {BLUE}Suggestions ({}/{}){RESET}", current_page + 1, total_pages);
2818
+ for i in start_idx..end_idx {
2819
+ let (cmd, desc) = matches[i];
2515
2820
  if Some(i) == highlight_idx {
2516
2821
  println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
2517
2822
  } else {
@@ -2522,11 +2827,29 @@ fn draw_input_box(
2522
2827
  }
2523
2828
 
2524
2829
  let _ = io::stdout().flush();
2525
- match_count
2830
+ (match_count, start_idx)
2526
2831
  }
2527
2832
 
2528
2833
  fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
2529
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
2834
+ let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2835
+ let width = term_width as usize;
2836
+ let prefix = "› ";
2837
+ let input_width = width.saturating_sub(2);
2838
+ let content_max_len = input_width.saturating_sub(prefix.chars().count());
2839
+
2840
+ let mut start_idx = 0;
2841
+ if input_chars.len() > content_max_len {
2842
+ if cursor_pos >= content_max_len {
2843
+ start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
2844
+ if start_idx + content_max_len > input_chars.len() {
2845
+ start_idx = input_chars.len() - content_max_len;
2846
+ }
2847
+ }
2848
+ }
2849
+
2850
+ let cursor_pos_clamped = std::cmp::min(cursor_pos, input_chars.len());
2851
+ let start_idx_clamped = std::cmp::min(start_idx, cursor_pos_clamped);
2852
+ let visual_cursor_pos: usize = input_chars[start_idx_clamped..cursor_pos_clamped]
2530
2853
  .iter()
2531
2854
  .copied()
2532
2855
  .map(char_visual_width)
@@ -2557,9 +2880,9 @@ fn redraw_input_box(
2557
2880
  tab_index: Option<usize>,
2558
2881
  ) {
2559
2882
  clear_input_box();
2560
- let input: String = input_chars.iter().collect();
2561
- let match_count = draw_input_box(
2562
- &input,
2883
+ let (match_count, _) = draw_input_box(
2884
+ input_chars,
2885
+ cursor_pos,
2563
2886
  placeholder,
2564
2887
  model,
2565
2888
  path_str,
@@ -2590,7 +2913,7 @@ fn read_line_interactive(
2590
2913
  let mut tab_base_input: Option<String> = None;
2591
2914
  let mut tab_index: Option<usize> = None;
2592
2915
 
2593
- let match_count = draw_input_box("", placeholder, model, path_str, None, None);
2916
+ let (match_count, _) = draw_input_box(&input_chars, cursor_pos, placeholder, model, path_str, None, None);
2594
2917
  position_input_cursor(&input_chars, cursor_pos, match_count);
2595
2918
  let _ = io::stdout().flush();
2596
2919
 
@@ -2709,12 +3032,7 @@ fn read_line_interactive(
2709
3032
  }
2710
3033
  }
2711
3034
  KeyCode::Char(c) => {
2712
- let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2713
- let max_width = (term_width as usize).saturating_sub(4);
2714
- let current_visual_width: usize =
2715
- input_chars.iter().copied().map(char_visual_width).sum();
2716
-
2717
- if current_visual_width < max_width {
3035
+ if input_chars.len() < 10000 {
2718
3036
  input_chars.insert(cursor_pos, c);
2719
3037
  cursor_pos += 1;
2720
3038
 
@@ -2883,13 +3201,15 @@ fn read_line_interactive(
2883
3201
  }
2884
3202
  }
2885
3203
  disable_raw_mode()?;
2886
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
2887
- .iter()
2888
- .copied()
2889
- .map(char_visual_width)
2890
- .sum();
2891
- print!("\x1b[{}G", 4 + visual_cursor_pos);
2892
- let _ = io::stdout().flush();
3204
+ redraw_input_box(
3205
+ &input_chars,
3206
+ cursor_pos,
3207
+ placeholder,
3208
+ model,
3209
+ path_str,
3210
+ None,
3211
+ None,
3212
+ );
2893
3213
  enable_raw_mode()?;
2894
3214
  }
2895
3215
  KeyCode::Right => {
@@ -2902,13 +3222,15 @@ fn read_line_interactive(
2902
3222
  }
2903
3223
  }
2904
3224
  disable_raw_mode()?;
2905
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
2906
- .iter()
2907
- .copied()
2908
- .map(char_visual_width)
2909
- .sum();
2910
- print!("\x1b[{}G", 4 + visual_cursor_pos);
2911
- let _ = io::stdout().flush();
3225
+ redraw_input_box(
3226
+ &input_chars,
3227
+ cursor_pos,
3228
+ placeholder,
3229
+ model,
3230
+ path_str,
3231
+ None,
3232
+ None,
3233
+ );
2912
3234
  enable_raw_mode()?;
2913
3235
  }
2914
3236
  KeyCode::Enter => {
@@ -2926,7 +3248,7 @@ fn read_line_interactive(
2926
3248
  expanded_str = expanded_str.replace(placeholder_str, content);
2927
3249
  }
2928
3250
 
2929
- println!("{BLUE}You ›{RESET} {}", expanded_str);
3251
+ println!(" {BLUE}You ›{RESET} {}", expanded_str);
2930
3252
  let _ = io::stdout().flush();
2931
3253
 
2932
3254
  break Some(InteractiveInput {
@@ -3157,7 +3479,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3157
3479
  .to_string();
3158
3480
 
3159
3481
  if SESSION_APPROVED.load(std::sync::atomic::Ordering::Relaxed) {
3160
- println!("{} {MINT}Approve (session-wide){RESET}", clean_prompt);
3482
+ println!(" {} {MINT}Approve (session-wide){RESET}", clean_prompt);
3161
3483
  return Ok(true);
3162
3484
  }
3163
3485
 
@@ -3166,7 +3488,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3166
3488
  use crossterm::tty::IsTty;
3167
3489
 
3168
3490
  if !io::stdout().is_tty() || enable_raw_mode().is_err() {
3169
- print!("{} [y/N] ", clean_prompt);
3491
+ print!(" {} [y/N] ", clean_prompt);
3170
3492
  let _ = io::stdout().flush();
3171
3493
  let mut answer = String::new();
3172
3494
  io::stdin().read_line(&mut answer)?;
@@ -3177,7 +3499,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3177
3499
  }
3178
3500
 
3179
3501
  let _ = disable_raw_mode();
3180
- println!("{}", clean_prompt);
3502
+ println!(" {}", clean_prompt);
3181
3503
 
3182
3504
  let options = ["Approve", "Approve this session", "No"];
3183
3505
  let mut selected = 0;
@@ -3284,7 +3606,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3284
3606
  1 => format!("{MINT}Approve this session{RESET}"),
3285
3607
  _ => format!("{ERROR}No{RESET}"),
3286
3608
  };
3287
- println!("{} {}", clean_prompt, result_str);
3609
+ println!(" {} {}", clean_prompt, result_str);
3288
3610
  let _ = io::stdout().flush();
3289
3611
 
3290
3612
  match choice {
@@ -3357,6 +3679,7 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
3357
3679
  audio_data_uri: None,
3358
3680
  document_attachment: None,
3359
3681
  workspace_path: None,
3682
+ agent_id: None,
3360
3683
  },
3361
3684
  )
3362
3685
  .await?;