@pheem49/mint 1.8.1 → 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 (184) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +0 -17
  4. package/Release_Note.md +92 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +100 -6
  7. package/crates/mint-cli/src/main.rs +166 -52
  8. package/crates/mint-core/src/agent_loop.rs +1 -0
  9. package/crates/mint-core/src/api_server.rs +55 -2
  10. package/crates/mint-core/src/browser.rs +55 -0
  11. package/crates/mint-core/src/channels.rs +1 -0
  12. package/crates/mint-core/src/chat.rs +3 -0
  13. package/crates/mint-core/src/config.rs +61 -0
  14. package/crates/mint-core/src/lib.rs +14 -0
  15. package/crates/mint-core/src/orchestration.rs +180 -11
  16. package/crates/mint-core/src/tts.rs +3 -1
  17. package/flatpak/com.pheem49.mint.desktop +9 -0
  18. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  19. package/flatpak/com.pheem49.mint.yaml +52 -0
  20. package/package.json +1 -1
  21. package/shared-modules/.github/workflows/ci.yml +120 -0
  22. package/shared-modules/.github/workflows/update.yaml +28 -0
  23. package/shared-modules/CODEOWNERS +23 -0
  24. package/shared-modules/README.md +79 -0
  25. package/shared-modules/SDL/README.md +3 -0
  26. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  27. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  28. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  29. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  30. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  31. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  32. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  33. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  34. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  35. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  36. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  37. package/shared-modules/SDL/sdl12-compat.json +22 -0
  38. package/shared-modules/SDL2/SDL2_image.json +27 -0
  39. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  40. package/shared-modules/cld2/CMakeLists.txt +157 -0
  41. package/shared-modules/cld2/cld2.json +24 -0
  42. package/shared-modules/clutter/clutter.json +72 -0
  43. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  44. package/shared-modules/glew/glew.json +36 -0
  45. package/shared-modules/glu/glu-9.json +12 -0
  46. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  47. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  48. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  49. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  50. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  51. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  52. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  53. package/shared-modules/gtk2/gtk2.json +120 -0
  54. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  55. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  56. package/shared-modules/gtk2/readme.md +10 -0
  57. package/shared-modules/intltool/intltool-0.51.json +20 -0
  58. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  59. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  60. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  61. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  62. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  63. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  64. package/shared-modules/lame/lame-3.99.5.json +32 -0
  65. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  66. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  67. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  68. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  69. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  70. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  71. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  72. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  73. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  74. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  75. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  76. package/shared-modules/libcanberra/libcanberra.json +44 -0
  77. package/shared-modules/libmad/libmad.json +19 -0
  78. package/shared-modules/libsecret/libsecret.json +23 -0
  79. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  80. package/shared-modules/libusb/libusb.json +27 -0
  81. package/shared-modules/linux-audio/dssi.json +31 -0
  82. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  83. package/shared-modules/linux-audio/fftw3f.json +42 -0
  84. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  85. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  86. package/shared-modules/linux-audio/ladspa.json +26 -0
  87. package/shared-modules/linux-audio/lash.json +35 -0
  88. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  89. package/shared-modules/linux-audio/liblo-static.json +22 -0
  90. package/shared-modules/linux-audio/liblo.json +17 -0
  91. package/shared-modules/linux-audio/lilv-static.json +94 -0
  92. package/shared-modules/linux-audio/lilv.json +82 -0
  93. package/shared-modules/linux-audio/lrdf.json +57 -0
  94. package/shared-modules/linux-audio/lv2.json +21 -0
  95. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  96. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  97. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  98. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  99. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  100. package/shared-modules/linux-audio/stk.json +28 -0
  101. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  102. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  103. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  104. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  105. package/shared-modules/lua5.3/lua.pc.in +8 -0
  106. package/shared-modules/lua5.3/luaconf.patch +47 -0
  107. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  108. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  109. package/shared-modules/lua5.4/lua.pc.in +8 -0
  110. package/shared-modules/lua5.4/luaconf.patch +34 -0
  111. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  112. package/shared-modules/luajit/luajit.json +30 -0
  113. package/shared-modules/lzo/lzo.json +27 -0
  114. package/shared-modules/mac/mac.json +18 -0
  115. package/shared-modules/physfs/physfs.json +26 -0
  116. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  117. package/shared-modules/pygame/cython-fixes.patch +34 -0
  118. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  119. package/shared-modules/pygame/glib.patch +25 -0
  120. package/shared-modules/pygame/pygame1-python2.json +157 -0
  121. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  122. package/shared-modules/pygtk/pygtk.json +117 -0
  123. package/shared-modules/pygtk/pygtk.patch +39 -0
  124. package/shared-modules/python2.7/python-2.7.json +48 -0
  125. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  126. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  127. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  128. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  129. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  130. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  131. package/shared-modules/update-checker.sh +50 -0
  132. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  133. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  134. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  135. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  136. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  137. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  138. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  139. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  140. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  141. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  142. package/shared-modules/vorbisgain/README.md +23 -0
  143. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  144. package/src/renderer/shared/agentProgress.ts +145 -0
  145. package/src/renderer/shared/calculator.ts +52 -0
  146. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  147. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  148. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  149. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  150. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  151. package/src/renderer/shared/constants/models.ts +91 -0
  152. package/src/renderer/shared/platform.ts +82 -0
  153. package/src/renderer/shared/types.ts +140 -0
  154. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  155. package/src/renderer/shared/utils/approval.ts +1 -0
  156. package/src/renderer/shared/utils/approval.tsx +64 -0
  157. package/src/renderer/shared/utils/markdown.tsx +347 -0
  158. package/src/renderer/shared/utils/providers.ts +36 -0
  159. package/src/renderer/shared/utils/speech.ts +216 -0
  160. package/src/renderer/shared/utils/ui.ts +147 -0
  161. package/src/renderer/src/agentProgress.ts +11 -39
  162. package/src/renderer/src/calculator.ts +3 -46
  163. package/src/renderer/src/components/ChatPanel.tsx +194 -1303
  164. package/src/renderer/src/components/MintDashboard.tsx +23 -169
  165. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  166. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  167. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  168. package/src/renderer/src/components/SettingsWindow.tsx +53 -3
  169. package/src/renderer/src/css/styles.css +84 -0
  170. package/src/renderer/src/tauri.ts +128 -143
  171. package/src/renderer/src-web/agentProgress.ts +11 -39
  172. package/src/renderer/src-web/calculator.ts +3 -46
  173. package/src/renderer/src-web/components/ChatPanel.tsx +194 -1301
  174. package/src/renderer/src-web/components/MintDashboard.tsx +23 -169
  175. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  176. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  177. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  178. package/src/renderer/src-web/components/SettingsWindow.tsx +53 -3
  179. package/src/renderer/src-web/css/styles.css +84 -0
  180. package/src/renderer/src-web/tauri.ts +180 -102
  181. package/src-tauri/src/lib.rs +137 -33
  182. package/src-tauri/src/webhooks.rs +1 -0
  183. package/src-tauri/src/workflows.rs +1 -0
  184. package/src-tauri/tauri.conf.json +1 -1
@@ -803,6 +803,7 @@ async fn main() -> Result<()> {
803
803
  audio_data_uri: None,
804
804
  document_attachment: None,
805
805
  workspace_path: None,
806
+ agent_id: None,
806
807
  },
807
808
  )
808
809
  .await?;
@@ -1761,8 +1762,12 @@ async fn handle_slash_command(
1761
1762
  ("/mcp list", "List configured MCP servers"),
1762
1763
  ("/mcp allow <server> <tool>", "Allow an MCP tool"),
1763
1764
  ("/stats", "Show session statistics"),
1764
- ("/exit | /quit", "Exit Mint"),
1765
+ ("/exit", "Exit Mint"),
1765
1766
  ("/code <task>", "Run in code-agent mode"),
1767
+ (
1768
+ "/multi-agent [on|off]",
1769
+ "Show or toggle Multi-Agent Collaboration system",
1770
+ ),
1766
1771
  ];
1767
1772
  for (cmd_name, desc) in &commands {
1768
1773
  println!(" {MINT}{:<30}{RESET} {DIM}{}{RESET}", cmd_name, desc);
@@ -1803,13 +1808,77 @@ async fn handle_slash_command(
1803
1808
  println!();
1804
1809
  } else {
1805
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";
1806
1842
  match mint_core::save_config(&session.config) {
1807
1843
  Ok(()) => println!(
1808
- "{DIM}Switched to provider: {}{RESET}\n",
1809
- session.config.ai_provider
1844
+ "{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
1845
+ if session.config.enable_agent_collaboration { "Enabled" } else { "Disabled" }
1810
1846
  ),
1811
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1847
+ Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1812
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!();
1813
1882
  }
1814
1883
  Some(SlashResult::Handled)
1815
1884
  }
@@ -2542,6 +2611,7 @@ async fn run_interactive_chat() -> Result<()> {
2542
2611
  audio_data_uri: None,
2543
2612
  document_attachment: None,
2544
2613
  workspace_path: None,
2614
+ agent_id: None,
2545
2615
  },
2546
2616
  |chunk| {
2547
2617
  if first_chunk {
@@ -2635,51 +2705,69 @@ fn print_exit_message(session: &InteractiveSession) {
2635
2705
  }
2636
2706
 
2637
2707
  const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
2638
- ("/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"),
2639
2712
  ("/fast", "Toggle fast mode (hide thinking traces)"),
2640
- ("/models", "List AI providers or switch active provider"),
2713
+ ("/help", "Show help menu"),
2714
+ ("/image", "Attach image from disk"),
2641
2715
  (
2642
2716
  "/image-provider",
2643
2717
  "List image gen providers or switch default provider",
2644
2718
  ),
2645
- ("/clear", "Clear conversation history"),
2646
- ("/cd", "Change active workspace directory"),
2647
- ("/image", "Attach image from disk"),
2648
- ("/paste", "Attach image from clipboard"),
2649
2719
  ("/learn", "Import persistent skill/instruction"),
2650
- ("/plugins", "List or generate plugins/skills"),
2651
- ("/memory", "Manage long-term memory store"),
2652
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"),
2653
2726
  ("/stats", "Show session statistics"),
2654
- ("/exit", "Exit Mint CLI"),
2655
- ("/quit", "Exit Mint CLI"),
2656
- ("/code", "Run in code-agent mode"),
2657
2727
  ];
2658
2728
 
2659
2729
  fn draw_input_box(
2660
- input: &str,
2730
+ input_chars: &[char],
2731
+ cursor_pos: usize,
2661
2732
  placeholder: &str,
2662
2733
  model: &str,
2663
2734
  path_str: &str,
2664
2735
  tab_base_input: Option<&str>,
2665
2736
  tab_index: Option<usize>,
2666
- ) -> usize {
2737
+ ) -> (usize, usize) {
2667
2738
  let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2668
2739
  let width = term_width as usize;
2669
2740
  let prefix = "› ";
2670
2741
  let input_width = width.saturating_sub(2);
2671
2742
  let content_max_len = input_width.saturating_sub(prefix.chars().count());
2672
2743
 
2673
- 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() {
2674
2762
  format!("{DIM}{}\x1b[39m", placeholder)
2675
2763
  } else {
2676
- format_placeholders(input)
2764
+ format_placeholders(&displayed_input)
2677
2765
  };
2678
2766
 
2679
- let visible_len = if input.is_empty() {
2767
+ let visible_len = if displayed_input.is_empty() {
2680
2768
  placeholder.chars().count()
2681
2769
  } else {
2682
- string_visual_width(input)
2770
+ string_visual_width(&displayed_input)
2683
2771
  };
2684
2772
 
2685
2773
  let pad_len = content_max_len.saturating_sub(visible_len);
@@ -2707,7 +2795,8 @@ fn draw_input_box(
2707
2795
  );
2708
2796
 
2709
2797
  // Compute and draw suggestions
2710
- 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);
2711
2800
  let mut match_count = 0;
2712
2801
  if search_query.starts_with('/') {
2713
2802
  let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
@@ -2716,11 +2805,18 @@ fn draw_input_box(
2716
2805
  .collect();
2717
2806
 
2718
2807
  if !matches.is_empty() {
2719
- match_count = matches.len();
2720
- println!();
2721
- println!(" {BLUE}Suggestions{RESET}");
2808
+ let total_pages = (matches.len() + 4) / 5;
2722
2809
  let highlight_idx = tab_index.map(|idx| idx % matches.len());
2723
- 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];
2724
2820
  if Some(i) == highlight_idx {
2725
2821
  println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
2726
2822
  } else {
@@ -2731,11 +2827,29 @@ fn draw_input_box(
2731
2827
  }
2732
2828
 
2733
2829
  let _ = io::stdout().flush();
2734
- match_count
2830
+ (match_count, start_idx)
2735
2831
  }
2736
2832
 
2737
2833
  fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
2738
- 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]
2739
2853
  .iter()
2740
2854
  .copied()
2741
2855
  .map(char_visual_width)
@@ -2766,9 +2880,9 @@ fn redraw_input_box(
2766
2880
  tab_index: Option<usize>,
2767
2881
  ) {
2768
2882
  clear_input_box();
2769
- let input: String = input_chars.iter().collect();
2770
- let match_count = draw_input_box(
2771
- &input,
2883
+ let (match_count, _) = draw_input_box(
2884
+ input_chars,
2885
+ cursor_pos,
2772
2886
  placeholder,
2773
2887
  model,
2774
2888
  path_str,
@@ -2799,7 +2913,7 @@ fn read_line_interactive(
2799
2913
  let mut tab_base_input: Option<String> = None;
2800
2914
  let mut tab_index: Option<usize> = None;
2801
2915
 
2802
- 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);
2803
2917
  position_input_cursor(&input_chars, cursor_pos, match_count);
2804
2918
  let _ = io::stdout().flush();
2805
2919
 
@@ -2918,12 +3032,7 @@ fn read_line_interactive(
2918
3032
  }
2919
3033
  }
2920
3034
  KeyCode::Char(c) => {
2921
- let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2922
- let max_width = (term_width as usize).saturating_sub(4);
2923
- let current_visual_width: usize =
2924
- input_chars.iter().copied().map(char_visual_width).sum();
2925
-
2926
- if current_visual_width < max_width {
3035
+ if input_chars.len() < 10000 {
2927
3036
  input_chars.insert(cursor_pos, c);
2928
3037
  cursor_pos += 1;
2929
3038
 
@@ -3092,13 +3201,15 @@ fn read_line_interactive(
3092
3201
  }
3093
3202
  }
3094
3203
  disable_raw_mode()?;
3095
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
3096
- .iter()
3097
- .copied()
3098
- .map(char_visual_width)
3099
- .sum();
3100
- print!("\x1b[{}G", 4 + visual_cursor_pos);
3101
- 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
+ );
3102
3213
  enable_raw_mode()?;
3103
3214
  }
3104
3215
  KeyCode::Right => {
@@ -3111,13 +3222,15 @@ fn read_line_interactive(
3111
3222
  }
3112
3223
  }
3113
3224
  disable_raw_mode()?;
3114
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
3115
- .iter()
3116
- .copied()
3117
- .map(char_visual_width)
3118
- .sum();
3119
- print!("\x1b[{}G", 4 + visual_cursor_pos);
3120
- 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
+ );
3121
3234
  enable_raw_mode()?;
3122
3235
  }
3123
3236
  KeyCode::Enter => {
@@ -3566,6 +3679,7 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
3566
3679
  audio_data_uri: None,
3567
3680
  document_attachment: None,
3568
3681
  workspace_path: None,
3682
+ agent_id: None,
3569
3683
  },
3570
3684
  )
3571
3685
  .await?;
@@ -61,6 +61,7 @@ where
61
61
  audio_data_uri: None,
62
62
  document_attachment: None,
63
63
  workspace_path: None,
64
+ agent_id: None,
64
65
  },
65
66
  )
66
67
  .await?;
@@ -476,6 +476,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
476
476
  image_data_uri: Option<String>,
477
477
  audio_data_uri: Option<String>,
478
478
  document_attachment: Option<crate::chat::DocumentAttachment>,
479
+ agent_id: Option<String>,
479
480
  }
480
481
 
481
482
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -488,6 +489,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
488
489
  audio_data_uri: req.audio_data_uri,
489
490
  document_attachment: req.document_attachment,
490
491
  workspace_path: None,
492
+ agent_id: req.agent_id,
491
493
  };
492
494
  let mut chat_req =
493
495
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -550,6 +552,24 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
550
552
  )
551
553
  .await;
552
554
  }
555
+ ("POST", "/api/cancel-chat") => {
556
+ #[derive(Deserialize)]
557
+ #[serde(rename_all = "camelCase")]
558
+ struct CancelRequest {
559
+ chat_id: String,
560
+ }
561
+ if let Ok(req) = serde_json::from_str::<CancelRequest>(body) {
562
+ crate::cancel_agent(&req.chat_id);
563
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
564
+ } else {
565
+ send_json_response(
566
+ socket,
567
+ "400 Bad Request",
568
+ "{\"status\":\"invalid cancel body\"}",
569
+ )
570
+ .await;
571
+ }
572
+ }
553
573
  ("POST", "/api/chat-stream") => {
554
574
  #[derive(Deserialize)]
555
575
  #[serde(rename_all = "camelCase")]
@@ -560,6 +580,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
560
580
  image_data_uri: Option<String>,
561
581
  audio_data_uri: Option<String>,
562
582
  document_attachment: Option<crate::chat::DocumentAttachment>,
583
+ agent_id: Option<String>,
563
584
  }
564
585
 
565
586
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -572,6 +593,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
572
593
  audio_data_uri: req.audio_data_uri,
573
594
  document_attachment: req.document_attachment,
574
595
  workspace_path: None,
596
+ agent_id: req.agent_id,
575
597
  };
576
598
  let mut chat_req =
577
599
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -631,7 +653,8 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
631
653
  let config_clone = config.clone();
632
654
  let chat_req_clone = chat_req.clone();
633
655
  let tx_done = tx.clone();
634
- tokio::spawn(async move {
656
+ let chat_id_str = chat_req.chat_id.clone().unwrap_or_default();
657
+ let join_handle = tokio::spawn(async move {
635
658
  let result = orchestrate_chat_stream_with_fallback(
636
659
  &config_clone,
637
660
  &chat_req_clone,
@@ -675,6 +698,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
675
698
  }
676
699
  }
677
700
  });
701
+
702
+ let abort_handle = join_handle.abort_handle();
703
+ if !chat_id_str.is_empty() {
704
+ crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
705
+ }
706
+
707
+ let chat_id_str_cleanup = chat_id_str.clone();
708
+ tokio::spawn(async move {
709
+ let _ = join_handle.await;
710
+ if !chat_id_str_cleanup.is_empty() {
711
+ crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
712
+ }
713
+ });
678
714
  } else {
679
715
  let root = std::env::current_dir().unwrap_or_default();
680
716
  let fast_mode = config
@@ -686,16 +722,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
686
722
  let tx_done = tx.clone();
687
723
  let config_clone = config.clone();
688
724
  let chat_id = chat_req.chat_id.clone();
725
+ let chat_id_str = chat_id.clone().unwrap_or_default();
689
726
  let message = chat_req.message.clone();
690
727
  let image_data_uri = chat_req.image_data_uri.clone();
728
+ let agent_id = chat_req.agent_id.clone();
691
729
 
692
- tokio::spawn(async move {
730
+ let join_handle = tokio::spawn(async move {
693
731
  let result = orchestrate_agent_loop(
694
732
  &config_clone,
695
733
  &message,
696
734
  &root,
697
735
  image_data_uri,
698
736
  chat_id.as_deref(),
737
+ agent_id.as_deref(),
699
738
  fast_mode,
700
739
  |_| Ok(ApprovalOutcome::Denied),
701
740
  progress_cb,
@@ -734,6 +773,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
734
773
  }
735
774
  }
736
775
  });
776
+
777
+ let abort_handle = join_handle.abort_handle();
778
+ if !chat_id_str.is_empty() {
779
+ crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
780
+ }
781
+
782
+ let chat_id_str_cleanup = chat_id_str.clone();
783
+ tokio::spawn(async move {
784
+ let _ = join_handle.await;
785
+ if !chat_id_str_cleanup.is_empty() {
786
+ crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
787
+ }
788
+ });
737
789
  }
738
790
  }
739
791
 
@@ -894,6 +946,7 @@ async fn run_web_agent_loop(
894
946
  &root,
895
947
  request.image_data_uri.clone(),
896
948
  request.chat_id.as_deref(),
949
+ request.agent_id.as_deref(),
897
950
  fast_mode,
898
951
  |_| Ok(ApprovalOutcome::Denied),
899
952
  |_| {},
@@ -340,6 +340,61 @@ async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Va
340
340
  let (mut socket, _) = connect_async(socket_url)
341
341
  .await
342
342
  .map_err(|error| format!("unable to connect to Chrome DevTools websocket: {error}"))?;
343
+ let is_aura_script = method == "Runtime.evaluate"
344
+ && params["expression"]
345
+ .as_str()
346
+ .map(|s| s.contains("mint-browser-aura"))
347
+ .unwrap_or(false);
348
+
349
+ if !is_aura_script {
350
+ let aura_script = r#"
351
+ (() => {
352
+ if (document.getElementById('mint-browser-aura')) return;
353
+ const aura = document.createElement('div');
354
+ aura.id = 'mint-browser-aura';
355
+
356
+ const style = document.createElement('style');
357
+ style.id = 'mint-browser-aura-style';
358
+ style.textContent = `
359
+ @keyframes mint-pulse {
360
+ 0% { box-shadow: inset 0 0 15px rgba(16, 185, 129, 0.4); border-color: rgba(16, 185, 129, 0.6); }
361
+ 100% { box-shadow: inset 0 0 30px rgba(16, 185, 129, 0.8); border-color: rgba(16, 185, 129, 1); }
362
+ }
363
+ `;
364
+ document.head.appendChild(style);
365
+
366
+ aura.style.cssText = `
367
+ position: fixed;
368
+ top: 0;
369
+ left: 0;
370
+ width: 100vw;
371
+ height: 100vh;
372
+ border: 5px solid rgba(16, 185, 129, 0.6);
373
+ pointer-events: none;
374
+ z-index: 2147483647;
375
+ box-sizing: border-box;
376
+ animation: mint-pulse 1.5s infinite alternate;
377
+ `;
378
+ document.body.appendChild(aura);
379
+ })()
380
+ "#;
381
+
382
+ let _ = socket
383
+ .send(Message::Text(
384
+ json!({
385
+ "id": 999,
386
+ "method": "Runtime.evaluate",
387
+ "params": {
388
+ "expression": aura_script,
389
+ "returnByValue": true
390
+ }
391
+ })
392
+ .to_string()
393
+ .into(),
394
+ ))
395
+ .await;
396
+ }
397
+
343
398
  socket
344
399
  .send(Message::Text(
345
400
  json!({ "id": 1, "method": method, "params": params })
@@ -194,6 +194,7 @@ async fn answer(text: &str, system_instruction: &str) -> String {
194
194
  audio_data_uri: None,
195
195
  document_attachment: None,
196
196
  workspace_path: None,
197
+ agent_id: None,
197
198
  },
198
199
  )
199
200
  .await
@@ -99,6 +99,8 @@ pub struct ChatRequest {
99
99
  pub document_attachment: Option<DocumentAttachment>,
100
100
  #[serde(default)]
101
101
  pub workspace_path: Option<String>,
102
+ #[serde(default)]
103
+ pub agent_id: Option<String>,
102
104
  }
103
105
 
104
106
  #[derive(Debug, Clone, Deserialize)]
@@ -965,6 +967,7 @@ mod tests {
965
967
  audio_data_uri: None,
966
968
  document_attachment: None,
967
969
  workspace_path: None,
970
+ agent_id: None,
968
971
  })
969
972
  .unwrap();
970
973
  assert_eq!(parts.len(), 3);
@@ -79,10 +79,24 @@ pub struct MintConfig {
79
79
  pub blocked_paths: Vec<PathBuf>,
80
80
  pub blocked_file_names: Vec<String>,
81
81
  pub disabled_tools: Vec<String>,
82
+ pub agents: Vec<AgentConfig>,
83
+ pub enable_agent_collaboration: bool,
82
84
  #[serde(flatten)]
83
85
  pub extra: BTreeMap<String, Value>,
84
86
  }
85
87
 
88
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
89
+ #[serde(rename_all = "camelCase")]
90
+ pub struct AgentConfig {
91
+ pub id: String,
92
+ pub name: String,
93
+ pub provider: String,
94
+ pub model: String,
95
+ pub api_key: Option<String>,
96
+ pub system_instruction: String,
97
+ pub enabled: bool,
98
+ }
99
+
86
100
  impl Default for MintConfig {
87
101
  fn default() -> Self {
88
102
  let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
@@ -142,12 +156,59 @@ impl Default for MintConfig {
142
156
  ],
143
157
  blocked_file_names: vec![".env".into(), "id_rsa".into(), "id_ed25519".into()],
144
158
  disabled_tools: Vec::new(),
159
+ agents: default_agents(),
160
+ enable_agent_collaboration: false,
145
161
  extra: runtime_extra_defaults(),
146
162
  }
147
163
  }
148
164
  }
149
165
 
166
+ fn default_agents() -> Vec<AgentConfig> {
167
+ vec![
168
+ AgentConfig {
169
+ id: "planner".into(),
170
+ name: "Planner".into(),
171
+ provider: "gemini".into(),
172
+ model: "gemini-2.5-flash".into(),
173
+ api_key: None,
174
+ system_instruction: "You are the Planner agent. Your task is to analyze the user request, inspect the workspace structure, and design a step-by-step implementation plan. Create or update the implementation_plan.md file to document your proposal.".into(),
175
+ enabled: true,
176
+ },
177
+ AgentConfig {
178
+ id: "coder".into(),
179
+ name: "Coder".into(),
180
+ provider: "gemini".into(),
181
+ model: "gemini-2.5-flash".into(),
182
+ api_key: None,
183
+ system_instruction: "You are the Coder agent. Your task is to implement the changes specified in the approved implementation plan. Read relevant files, write clean and efficient code, and run terminal commands to build or verify. Stay focused on execution.".into(),
184
+ enabled: true,
185
+ },
186
+ AgentConfig {
187
+ id: "reviewer".into(),
188
+ name: "Reviewer".into(),
189
+ provider: "gemini".into(),
190
+ model: "gemini-2.5-flash".into(),
191
+ api_key: None,
192
+ system_instruction: "You are the Reviewer agent. Your task is to verify the code modifications made by the Coder. Run the automated tests, check lint errors, and ensure the implementation is correct and complete.".into(),
193
+ enabled: true,
194
+ },
195
+ ]
196
+ }
197
+
150
198
  impl MintConfig {
199
+ pub fn active_model(&self) -> &str {
200
+ match self.ai_provider.as_str() {
201
+ "anthropic" => &self.anthropic_model,
202
+ "openai" => &self.openai_model,
203
+ "openrouter" => &self.openrouter_model,
204
+ "deepseek" => &self.deepseek_model,
205
+ "huggingface" => &self.hf_model,
206
+ "local_openai" => &self.local_model_name,
207
+ "ollama" => &self.ollama_model,
208
+ _ => &self.gemini_model,
209
+ }
210
+ }
211
+
151
212
  pub fn available_providers(&self) -> Vec<&'static str> {
152
213
  let mut providers = Vec::new();
153
214
  if has_value(&self.anthropic_api_key) {