@pheem49/mint 1.8.1 → 1.9.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 (200) 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 +137 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +150 -45
  7. package/crates/mint-cli/src/image.rs +8 -8
  8. package/crates/mint-cli/src/main.rs +929 -412
  9. package/crates/mint-cli/src/onboard.rs +108 -108
  10. package/crates/mint-cli/src/setup.rs +143 -143
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +108 -52
  13. package/crates/mint-core/src/browser.rs +56 -1
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +126 -8
  16. package/crates/mint-core/src/code_tools.rs +18 -20
  17. package/crates/mint-core/src/config.rs +245 -17
  18. package/crates/mint-core/src/image_gen.rs +5 -5
  19. package/crates/mint-core/src/lib.rs +16 -2
  20. package/crates/mint-core/src/memory.rs +6 -1
  21. package/crates/mint-core/src/orchestration.rs +260 -72
  22. package/crates/mint-core/src/pictures.rs +3 -4
  23. package/crates/mint-core/src/semantic.rs +1 -1
  24. package/crates/mint-core/src/shell.rs +1 -1
  25. package/crates/mint-core/src/skills.rs +46 -19
  26. package/crates/mint-core/src/tts.rs +3 -1
  27. package/flatpak/com.pheem49.mint.desktop +9 -0
  28. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  29. package/flatpak/com.pheem49.mint.yaml +52 -0
  30. package/package.json +1 -1
  31. package/shared-modules/.github/workflows/ci.yml +120 -0
  32. package/shared-modules/.github/workflows/update.yaml +28 -0
  33. package/shared-modules/CODEOWNERS +23 -0
  34. package/shared-modules/README.md +79 -0
  35. package/shared-modules/SDL/README.md +3 -0
  36. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  37. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  38. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  39. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  40. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  41. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  42. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  43. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  44. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  45. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  46. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  47. package/shared-modules/SDL/sdl12-compat.json +22 -0
  48. package/shared-modules/SDL2/SDL2_image.json +27 -0
  49. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  50. package/shared-modules/cld2/CMakeLists.txt +157 -0
  51. package/shared-modules/cld2/cld2.json +24 -0
  52. package/shared-modules/clutter/clutter.json +72 -0
  53. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  54. package/shared-modules/glew/glew.json +36 -0
  55. package/shared-modules/glu/glu-9.json +12 -0
  56. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  57. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  58. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  59. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  60. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  61. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  62. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  63. package/shared-modules/gtk2/gtk2.json +120 -0
  64. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  65. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  66. package/shared-modules/gtk2/readme.md +10 -0
  67. package/shared-modules/intltool/intltool-0.51.json +20 -0
  68. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  69. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  70. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  71. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  72. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  73. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  74. package/shared-modules/lame/lame-3.99.5.json +32 -0
  75. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  76. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  77. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  78. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  79. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  80. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  81. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  82. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  83. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  84. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  85. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  86. package/shared-modules/libcanberra/libcanberra.json +44 -0
  87. package/shared-modules/libmad/libmad.json +19 -0
  88. package/shared-modules/libsecret/libsecret.json +23 -0
  89. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  90. package/shared-modules/libusb/libusb.json +27 -0
  91. package/shared-modules/linux-audio/dssi.json +31 -0
  92. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  93. package/shared-modules/linux-audio/fftw3f.json +42 -0
  94. package/shared-modules/linux-audio/fluidsynth2-static.json +36 -0
  95. package/shared-modules/linux-audio/fluidsynth2.json +25 -0
  96. package/shared-modules/linux-audio/ladspa.json +26 -0
  97. package/shared-modules/linux-audio/lash.json +35 -0
  98. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  99. package/shared-modules/linux-audio/liblo-static.json +22 -0
  100. package/shared-modules/linux-audio/liblo.json +17 -0
  101. package/shared-modules/linux-audio/lilv-static.json +94 -0
  102. package/shared-modules/linux-audio/lilv.json +89 -0
  103. package/shared-modules/linux-audio/lrdf.json +57 -0
  104. package/shared-modules/linux-audio/lv2.json +20 -0
  105. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  106. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  107. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  108. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  109. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  110. package/shared-modules/linux-audio/stk.json +28 -0
  111. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  112. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  113. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  114. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  115. package/shared-modules/lua5.3/lua.pc.in +8 -0
  116. package/shared-modules/lua5.3/luaconf.patch +47 -0
  117. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  118. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  119. package/shared-modules/lua5.4/lua.pc.in +8 -0
  120. package/shared-modules/lua5.4/luaconf.patch +34 -0
  121. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  122. package/shared-modules/luajit/luajit.json +30 -0
  123. package/shared-modules/lzo/lzo.json +27 -0
  124. package/shared-modules/mac/mac.json +18 -0
  125. package/shared-modules/physfs/physfs.json +26 -0
  126. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  127. package/shared-modules/pygame/cython-fixes.patch +34 -0
  128. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  129. package/shared-modules/pygame/glib.patch +25 -0
  130. package/shared-modules/pygame/pygame1-python2.json +157 -0
  131. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  132. package/shared-modules/pygtk/pygtk.json +117 -0
  133. package/shared-modules/pygtk/pygtk.patch +39 -0
  134. package/shared-modules/python2.7/python-2.7.json +48 -0
  135. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  136. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  137. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  138. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  139. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  140. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  141. package/shared-modules/update-checker.sh +50 -0
  142. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  143. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  144. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  145. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  146. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  147. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  148. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  149. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  150. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  151. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  152. package/shared-modules/vorbisgain/README.md +23 -0
  153. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  154. package/src/renderer/shared/agentProgress.ts +145 -0
  155. package/src/renderer/shared/calculator.ts +52 -0
  156. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  157. package/src/renderer/shared/components/AgentActivityTable.tsx +85 -0
  158. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  159. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  160. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  161. package/src/renderer/shared/constants/models.ts +91 -0
  162. package/src/renderer/shared/platform.ts +82 -0
  163. package/src/renderer/shared/types.ts +140 -0
  164. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  165. package/src/renderer/shared/utils/approval.ts +1 -0
  166. package/src/renderer/shared/utils/approval.tsx +64 -0
  167. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  168. package/src/renderer/shared/utils/markdown.tsx +347 -0
  169. package/src/renderer/shared/utils/providers.ts +36 -0
  170. package/src/renderer/shared/utils/speech.ts +216 -0
  171. package/src/renderer/shared/utils/ui.ts +147 -0
  172. package/src/renderer/src/agentProgress.ts +11 -39
  173. package/src/renderer/src/calculator.ts +3 -46
  174. package/src/renderer/src/components/ChatPanel.tsx +219 -1308
  175. package/src/renderer/src/components/MintDashboard.tsx +67 -170
  176. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  177. package/src/renderer/src/components/Settings/AgentsTab.tsx +381 -0
  178. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  179. package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
  180. package/src/renderer/src/components/SettingsWindow.tsx +120 -3
  181. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  182. package/src/renderer/src/css/styles.css +100 -0
  183. package/src/renderer/src/tauri.ts +128 -143
  184. package/src/renderer/src-web/agentProgress.ts +11 -39
  185. package/src/renderer/src-web/calculator.ts +3 -46
  186. package/src/renderer/src-web/components/ChatPanel.tsx +219 -1306
  187. package/src/renderer/src-web/components/MintDashboard.tsx +42 -170
  188. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  189. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +381 -0
  190. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  191. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
  192. package/src/renderer/src-web/components/SettingsWindow.tsx +127 -3
  193. package/src/renderer/src-web/css/styles.css +100 -0
  194. package/src/renderer/src-web/tauri.ts +180 -102
  195. package/src-tauri/src/desktop.rs +18 -16
  196. package/src-tauri/src/discord_rpc.rs +1 -1
  197. package/src-tauri/src/lib.rs +168 -40
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +1 -0
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -494,8 +494,8 @@ async fn main() -> Result<()> {
494
494
  println!("{}", serde_json::to_string_pretty(&load_config()?)?)
495
495
  }
496
496
  ConfigCommand::Set { key, value } => {
497
- let value = serde_json::from_str(&value)
498
- .unwrap_or_else(|_| serde_json::Value::String(value));
497
+ let value =
498
+ serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
499
499
  println!(
500
500
  "{}",
501
501
  serde_json::to_string_pretty(&set_config_value(&key, value)?)?
@@ -579,7 +579,7 @@ async fn main() -> Result<()> {
579
579
 
580
580
  // Start tailing the log file in a background task
581
581
  let log_dir = dirs::config_dir()
582
- .unwrap_or_else(|| std::env::temp_dir())
582
+ .unwrap_or_else(std::env::temp_dir)
583
583
  .join("mint");
584
584
  let log_file = log_dir.join("browser-automation.log");
585
585
 
@@ -591,40 +591,37 @@ async fn main() -> Result<()> {
591
591
  use std::io::{BufRead, BufReader, Seek, SeekFrom};
592
592
  let mut file_pos = 0;
593
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
- }
594
+ if log_file_clone.exists()
595
+ && let Ok(mut file) = std::fs::File::open(&log_file_clone)
596
+ && file.seek(SeekFrom::Start(file_pos)).is_ok()
597
+ {
598
+ let reader = BufReader::new(file);
599
+ for line_str in reader.lines().flatten() {
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}");
626
620
  }
627
621
  }
622
+ if let Ok(pos) = log_file_clone.metadata().map(|m| m.len()) {
623
+ file_pos = pos;
624
+ }
628
625
  }
629
626
  tokio::time::sleep(std::time::Duration::from_millis(100)).await;
630
627
  }
@@ -705,20 +702,20 @@ async fn main() -> Result<()> {
705
702
  home.join(".config").join("mint").join("mint-skills");
706
703
  mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
707
704
  }
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
- );
705
+ if let Ok(root) = std::env::current_dir()
706
+ && let Ok(root) = root.canonicalize()
707
+ {
708
+ let workspace_skills_path1 = root.join(".agents").join("skills");
709
+ mint_core::skills::load_skills_from_dir(
710
+ &workspace_skills_path1,
711
+ &mut skills,
712
+ );
715
713
 
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
- }
714
+ let workspace_skills_path2 = root.join("skills");
715
+ mint_core::skills::load_skills_from_dir(
716
+ &workspace_skills_path2,
717
+ &mut skills,
718
+ );
722
719
  }
723
720
 
724
721
  let mut unique_skills = std::collections::BTreeMap::new();
@@ -803,6 +800,7 @@ async fn main() -> Result<()> {
803
800
  audio_data_uri: None,
804
801
  document_attachment: None,
805
802
  workspace_path: None,
803
+ agent_id: None,
806
804
  },
807
805
  )
808
806
  .await?;
@@ -1152,10 +1150,10 @@ async fn main() -> Result<()> {
1152
1150
  ),
1153
1151
  }
1154
1152
  }
1155
- if let Some(desc) = &result.description {
1156
- if !desc.is_empty() {
1157
- println!("\n{DIM}{desc}{RESET}");
1158
- }
1153
+ if let Some(desc) = &result.description
1154
+ && !desc.is_empty()
1155
+ {
1156
+ println!("\n{DIM}{desc}{RESET}");
1159
1157
  }
1160
1158
  }
1161
1159
  Err(e) => eprintln!("{ERROR}Failed to save images: {e}{RESET}"),
@@ -1200,16 +1198,16 @@ async fn launch_mint_target(target: String) -> Result<()> {
1200
1198
  path = p.parent();
1201
1199
  }
1202
1200
  }
1203
- if found.is_none() {
1204
- if let Ok(cwd) = std::env::current_dir() {
1205
- let mut path = Some(cwd.as_path());
1206
- while let Some(p) = path {
1207
- if p.join("package.json").exists() {
1208
- found = Some(p.to_path_buf());
1209
- break;
1210
- }
1211
- path = p.parent();
1201
+ if found.is_none()
1202
+ && let Ok(cwd) = std::env::current_dir()
1203
+ {
1204
+ let mut path = Some(cwd.as_path());
1205
+ while let Some(p) = path {
1206
+ if p.join("package.json").exists() {
1207
+ found = Some(p.to_path_buf());
1208
+ break;
1212
1209
  }
1210
+ path = p.parent();
1213
1211
  }
1214
1212
  }
1215
1213
  found.ok_or_else(|| {
@@ -1218,7 +1216,7 @@ async fn launch_mint_target(target: String) -> Result<()> {
1218
1216
  };
1219
1217
  std::process::Command::new("npm")
1220
1218
  .current_dir(&project_root)
1221
- .args(&["run", "dev:web"])
1219
+ .args(["run", "dev:web"])
1222
1220
  .stdout(std::process::Stdio::null())
1223
1221
  .stderr(std::process::Stdio::null())
1224
1222
  .spawn()
@@ -1401,7 +1399,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
1401
1399
  rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
1402
1400
 
1403
1401
  if let Some(ref header) = header_row {
1404
- rendered.push_str("│");
1402
+ rendered.push('│');
1405
1403
  for (i, col) in header.iter().enumerate() {
1406
1404
  let width = unicode_width(col);
1407
1405
  let padding = col_widths[i] - width;
@@ -1410,20 +1408,20 @@ fn render_markdown_table(table_lines: &[String]) -> String {
1410
1408
  col,
1411
1409
  " ".repeat(padding + 1)
1412
1410
  ));
1413
- rendered.push_str("│");
1411
+ rendered.push('│');
1414
1412
  }
1415
1413
  rendered.push('\n');
1416
1414
  rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
1417
1415
  }
1418
1416
 
1419
1417
  for (r_idx, row) in data_rows.iter().enumerate() {
1420
- rendered.push_str("│");
1418
+ rendered.push('│');
1421
1419
  for i in 0..num_cols {
1422
1420
  let col_val = row.get(i).cloned().unwrap_or_default();
1423
1421
  let width = unicode_width(&col_val);
1424
1422
  let padding = col_widths[i] - width;
1425
1423
  rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
1426
- rendered.push_str("│");
1424
+ rendered.push('│');
1427
1425
  }
1428
1426
  rendered.push('\n');
1429
1427
 
@@ -1432,7 +1430,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
1432
1430
  }
1433
1431
  }
1434
1432
 
1435
- rendered.push_str(&draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
1433
+ rendered.push_str(draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
1436
1434
  rendered
1437
1435
  }
1438
1436
 
@@ -1746,6 +1744,10 @@ async fn handle_slash_command(
1746
1744
  ("/paste [prompt]", "Attach image from clipboard"),
1747
1745
  ("Ctrl+V", "Paste clipboard image as [Image #1]"),
1748
1746
  ("/learn <path>", "Import a persistent .md or .txt skill"),
1747
+ (
1748
+ "/skill add <path>",
1749
+ "Copy/install skill file or folder to global config",
1750
+ ),
1749
1751
  (
1750
1752
  "/plugins <name> [prompt]",
1751
1753
  "Generate a skill.md file for a plugin/skill",
@@ -1761,8 +1763,12 @@ async fn handle_slash_command(
1761
1763
  ("/mcp list", "List configured MCP servers"),
1762
1764
  ("/mcp allow <server> <tool>", "Allow an MCP tool"),
1763
1765
  ("/stats", "Show session statistics"),
1764
- ("/exit | /quit", "Exit Mint"),
1766
+ ("/exit", "Exit Mint"),
1765
1767
  ("/code <task>", "Run in code-agent mode"),
1768
+ (
1769
+ "/multi-agent [on|off]",
1770
+ "Show or toggle Multi-Agent Collaboration system",
1771
+ ),
1766
1772
  ];
1767
1773
  for (cmd_name, desc) in &commands {
1768
1774
  println!(" {MINT}{:<30}{RESET} {DIM}{}{RESET}", cmd_name, desc);
@@ -1803,13 +1809,85 @@ async fn handle_slash_command(
1803
1809
  println!();
1804
1810
  } else {
1805
1811
  session.config.ai_provider = rest.to_owned();
1812
+ match mint_core::save_config(&session.config) {
1813
+ Ok(()) => {
1814
+ let active_model =
1815
+ active_model(&session.config.ai_provider, &session.config);
1816
+ let display_name = format!(
1817
+ "Changed model to {} • {}",
1818
+ session.config.ai_provider, active_model
1819
+ );
1820
+
1821
+ // Save system event interaction in memory
1822
+ if let Ok(memory) = MemoryStore::open_default() {
1823
+ let _ = memory.add_interaction_for_chat_with_fallback(
1824
+ CHAT_CLI_ID,
1825
+ &display_name,
1826
+ "",
1827
+ "system",
1828
+ "provider_change",
1829
+ None,
1830
+ );
1831
+ }
1832
+
1833
+ println!(
1834
+ "\n{DIM}───{RESET} {MINT}{}{RESET} {DIM}───{RESET}\n",
1835
+ display_name
1836
+ );
1837
+ }
1838
+ Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1839
+ }
1840
+ }
1841
+ Some(SlashResult::Handled)
1842
+ }
1843
+
1844
+ "/multi-agent" => {
1845
+ if rest == "on" || rest == "off" {
1846
+ session.config.enable_agent_collaboration = rest == "on";
1806
1847
  match mint_core::save_config(&session.config) {
1807
1848
  Ok(()) => println!(
1808
- "{DIM}Switched to provider: {}{RESET}\n",
1809
- session.config.ai_provider
1849
+ "{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
1850
+ if session.config.enable_agent_collaboration {
1851
+ "Enabled"
1852
+ } else {
1853
+ "Disabled"
1854
+ }
1810
1855
  ),
1811
- Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
1856
+ Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
1812
1857
  }
1858
+ } else if !rest.is_empty() {
1859
+ println!("{WARN}Usage: /multi-agent [on|off]{RESET}\n");
1860
+ } else {
1861
+ println!("\n{BLUE}Multi-Agent System Settings:{RESET}");
1862
+ let collab_status = if session.config.enable_agent_collaboration {
1863
+ format!("{MINT}Enabled (on){RESET}")
1864
+ } else {
1865
+ format!("{DIM}Disabled (off){RESET}")
1866
+ };
1867
+ println!(" Global Collaboration: {collab_status}");
1868
+ println!("\n{BLUE}Configured Agents:{RESET}");
1869
+ if session.config.agents.is_empty() {
1870
+ println!(" No agents configured.");
1871
+ } else {
1872
+ for agent in &session.config.agents {
1873
+ let status_label = if agent.enabled {
1874
+ format!("{MINT}[Enabled]{RESET}")
1875
+ } else {
1876
+ format!("{DIM}[Disabled]{RESET}")
1877
+ };
1878
+ println!(
1879
+ " {:<15} {} Provider: {:<12} Model: {}",
1880
+ agent.name, status_label, agent.provider, agent.model
1881
+ );
1882
+ let truncated_instruction = if agent.system_instruction.len() > 60 {
1883
+ format!("{}...", &agent.system_instruction[..60].replace('\n', " "))
1884
+ } else {
1885
+ agent.system_instruction.replace('\n', " ")
1886
+ };
1887
+ println!(" {DIM}Instruction: {truncated_instruction}{RESET}");
1888
+ }
1889
+ }
1890
+ println!();
1813
1891
  }
1814
1892
  Some(SlashResult::Handled)
1815
1893
  }
@@ -2024,6 +2102,73 @@ async fn handle_slash_command(
2024
2102
  Some(SlashResult::Handled)
2025
2103
  }
2026
2104
 
2105
+ "/skill" => {
2106
+ let (subcmd, args) = rest
2107
+ .split_once(char::is_whitespace)
2108
+ .map(|(c, a)| (c, a.trim()))
2109
+ .unwrap_or((rest, ""));
2110
+
2111
+ match subcmd {
2112
+ "add" | "install" => {
2113
+ if args.is_empty() {
2114
+ println!(
2115
+ "{WARN}/skill add <path> requires a path to a skill file or folder{RESET}\n"
2116
+ );
2117
+ } else {
2118
+ let source_path = PathBuf::from(args);
2119
+ let source_path = if source_path.is_absolute() {
2120
+ source_path
2121
+ } else {
2122
+ session.current_dir.join(source_path)
2123
+ };
2124
+
2125
+ if !source_path.exists() {
2126
+ println!("{ERROR}Source skill path not found: {}{RESET}\n", args);
2127
+ } else if let Some(home) = dirs::home_dir() {
2128
+ let global_skills_path =
2129
+ home.join(".config").join("mint").join("mint-skills");
2130
+ if !global_skills_path.exists() {
2131
+ let _ = std::fs::create_dir_all(&global_skills_path);
2132
+ }
2133
+
2134
+ let name = source_path
2135
+ .file_name()
2136
+ .and_then(|n| n.to_str())
2137
+ .unwrap_or("skill");
2138
+
2139
+ let dest_path = global_skills_path.join(name);
2140
+ let copy_res = if source_path.is_dir() {
2141
+ copy_dir_all(&source_path, &dest_path)
2142
+ } else {
2143
+ std::fs::copy(&source_path, &dest_path)
2144
+ .map(|_| ())
2145
+ .map_err(|e| e)
2146
+ };
2147
+
2148
+ match copy_res {
2149
+ Ok(()) => println!(
2150
+ "{DIM}Skill successfully copied to Global config: {}{RESET}\n",
2151
+ dest_path.display()
2152
+ ),
2153
+ Err(e) => println!(
2154
+ "{ERROR}Failed to copy skill to Global: {}{RESET}\n",
2155
+ e
2156
+ ),
2157
+ }
2158
+ } else {
2159
+ println!(
2160
+ "{ERROR}Unable to resolve home directory for Global config.{RESET}\n"
2161
+ );
2162
+ }
2163
+ }
2164
+ }
2165
+ _ => {
2166
+ println!("{WARN}Usage: /skill add <path> (or /skill install <path>){RESET}\n");
2167
+ }
2168
+ }
2169
+ Some(SlashResult::Handled)
2170
+ }
2171
+
2027
2172
  "/memory" => {
2028
2173
  let memory = match MemoryStore::open_default() {
2029
2174
  Ok(m) => m,
@@ -2222,44 +2367,40 @@ async fn handle_slash_command(
2222
2367
 
2223
2368
  // Scan local workspace skills directory
2224
2369
  let local_skills_dir = session.current_dir.join("skills");
2225
- if local_skills_dir.is_dir() {
2226
- if let Ok(entries) = fs::read_dir(&local_skills_dir) {
2227
- let mut local_files = Vec::new();
2228
- for entry in entries.flatten() {
2229
- let path = entry.path();
2230
- if path.is_file() {
2231
- if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2232
- if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
2233
- if let Some(name) =
2234
- path.file_name().and_then(|n| n.to_str())
2235
- {
2236
- if let Ok(canon_path) = path.canonicalize() {
2237
- local_files.push((name.to_owned(), canon_path));
2238
- } else {
2239
- local_files.push((name.to_owned(), path));
2240
- }
2241
- }
2242
- }
2243
- }
2370
+ if local_skills_dir.is_dir()
2371
+ && let Ok(entries) = fs::read_dir(&local_skills_dir)
2372
+ {
2373
+ let mut local_files = Vec::new();
2374
+ for entry in entries.flatten() {
2375
+ let path = entry.path();
2376
+ if path.is_file()
2377
+ && let Some(ext) = path.extension().and_then(|e| e.to_str())
2378
+ && matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt")
2379
+ && let Some(name) = path.file_name().and_then(|n| n.to_str())
2380
+ {
2381
+ if let Ok(canon_path) = path.canonicalize() {
2382
+ local_files.push((name.to_owned(), canon_path));
2383
+ } else {
2384
+ local_files.push((name.to_owned(), path));
2244
2385
  }
2245
2386
  }
2387
+ }
2246
2388
 
2247
- let inactive_files: Vec<_> = local_files
2248
- .into_iter()
2249
- .filter(|(_, full_path)| !active_skills.contains(full_path))
2250
- .map(|(name, _)| name)
2251
- .collect();
2389
+ let inactive_files: Vec<_> = local_files
2390
+ .into_iter()
2391
+ .filter(|(_, full_path)| !active_skills.contains(full_path))
2392
+ .map(|(name, _)| name)
2393
+ .collect();
2252
2394
 
2253
- if !inactive_files.is_empty() {
2395
+ if !inactive_files.is_empty() {
2396
+ println!(
2397
+ "\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}"
2398
+ );
2399
+ for file in inactive_files {
2254
2400
  println!(
2255
- "\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}"
2401
+ " - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
2402
+ file, file, file
2256
2403
  );
2257
- for file in inactive_files {
2258
- println!(
2259
- " - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
2260
- file, file, file
2261
- );
2262
- }
2263
2404
  }
2264
2405
  }
2265
2406
  }
@@ -2318,7 +2459,8 @@ async fn run_interactive_chat() -> Result<()> {
2318
2459
  now.format("%M")
2319
2460
  );
2320
2461
  let version = env!("CARGO_PKG_VERSION");
2321
- let line1_text = format!("[Mint] v{} | Active AI: {}", version, provider);
2462
+ let clean_provider_name = format_provider_display_name(provider, &config);
2463
+ let line1_text = format!("[Mint] v{} | Active AI: {}", version, clean_provider_name);
2322
2464
  let line2_text = format!("{} • {}", date_time, model);
2323
2465
 
2324
2466
  let len1 = line1_text.chars().count();
@@ -2340,7 +2482,7 @@ async fn run_interactive_chat() -> Result<()> {
2340
2482
  println!(
2341
2483
  "{MINT}| \\/ (_)_ __ | |_ / __| | |_ _|{RESET} {DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
2342
2484
  version,
2343
- provider,
2485
+ clean_provider_name,
2344
2486
  " ".repeat(content_width - len1)
2345
2487
  );
2346
2488
  println!(
@@ -2357,7 +2499,7 @@ async fn run_interactive_chat() -> Result<()> {
2357
2499
  println!(
2358
2500
  "{DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
2359
2501
  version,
2360
- provider,
2502
+ clean_provider_name,
2361
2503
  " ".repeat(content_width - len1)
2362
2504
  );
2363
2505
  println!(
@@ -2397,11 +2539,11 @@ async fn run_interactive_chat() -> Result<()> {
2397
2539
  loop {
2398
2540
  if let Some(handle) = update_handle.take() {
2399
2541
  if handle.is_finished() {
2400
- if let Ok(Some((current, latest))) = handle.await {
2401
- if !printed_update {
2402
- updater::print_update_notice(&current, &latest);
2403
- printed_update = true;
2404
- }
2542
+ if let Ok(Some((current, latest))) = handle.await
2543
+ && !printed_update
2544
+ {
2545
+ updater::print_update_notice(&current, &latest);
2546
+ printed_update = true;
2405
2547
  }
2406
2548
  } else {
2407
2549
  update_handle = Some(handle);
@@ -2411,9 +2553,12 @@ async fn run_interactive_chat() -> Result<()> {
2411
2553
  let path_str = format_path_with_tilde(&session.current_dir);
2412
2554
  let model_str = active_model(&session.config.ai_provider, &session.config).to_owned();
2413
2555
 
2414
- if let Some(input) =
2415
- read_line_interactive(&session.config.ai_provider, &model_str, &path_str)?
2416
- {
2556
+ if let Some(input) = read_line_interactive(
2557
+ &session.config.ai_provider,
2558
+ &model_str,
2559
+ &path_str,
2560
+ &session.current_dir,
2561
+ )? {
2417
2562
  if let Some(uri) = input.pasted_image {
2418
2563
  if let Some(ref mut current) = session.pending_image {
2419
2564
  current.push(' ');
@@ -2427,6 +2572,72 @@ async fn run_interactive_chat() -> Result<()> {
2427
2572
  continue;
2428
2573
  }
2429
2574
 
2575
+ if query_str.starts_with('$') {
2576
+ let (skill_word, task_part) = query_str
2577
+ .split_once(char::is_whitespace)
2578
+ .map(|(s, t)| (s, t.trim()))
2579
+ .unwrap_or((&query_str, ""));
2580
+
2581
+ let skill_name = skill_word.trim_start_matches('$').to_lowercase();
2582
+ let skills = load_all_available_skills(&session.current_dir);
2583
+ let skill_opt = skills.iter().find(|s| s.name.to_lowercase() == skill_name);
2584
+
2585
+ if let Some(skill) = skill_opt {
2586
+ println!("\n{BLUE}Skill: {}{RESET}", skill.name);
2587
+ if let Some(ref desc) = skill.description {
2588
+ println!("{DIM}{}{RESET}", desc);
2589
+ }
2590
+ println!("{DIM}────────────────────────────────────────────{RESET}");
2591
+ println!("{}", skill.content);
2592
+ println!("{DIM}────────────────────────────────────────────{RESET}\n");
2593
+
2594
+ if confirm("ต้องการ activate skill นี้ไหม? [y/N] ")? {
2595
+ let final_task = if task_part.is_empty() {
2596
+ print!("พิมพ์ Task ที่ต้องการให้ทำงานด้วย Skill นี้: ");
2597
+ let _ = io::stdout().flush();
2598
+ let mut input = String::new();
2599
+ io::stdin().read_line(&mut input)?;
2600
+ let input = input.trim().to_owned();
2601
+ if input.is_empty() {
2602
+ println!("{WARN}Cancelled: Task cannot be empty.{RESET}\n");
2603
+ continue;
2604
+ }
2605
+ input
2606
+ } else {
2607
+ task_part.to_owned()
2608
+ };
2609
+
2610
+ let task_with_skill = format!(
2611
+ "=== ACTIVATED SKILL: {} ===\n\
2612
+ {}\n\
2613
+ ===========================\n\n\
2614
+ Task: {}",
2615
+ skill.name, skill.content, final_task
2616
+ );
2617
+
2618
+ println!();
2619
+ if let Err(error) = run_code_agent_with_saved_image(
2620
+ &task_with_skill,
2621
+ &session.current_dir,
2622
+ &session.config,
2623
+ session.pending_image.take(),
2624
+ agent::AgentOptions {
2625
+ fast_mode: session.fast_mode,
2626
+ },
2627
+ )
2628
+ .await
2629
+ {
2630
+ println!("{ERROR}Error:{RESET} {error}\n");
2631
+ }
2632
+ } else {
2633
+ println!("{DIM}Cancelled.{RESET}\n");
2634
+ }
2635
+ } else {
2636
+ println!("{ERROR}Skill '{}' not found.{RESET}\n", skill_name);
2637
+ }
2638
+ continue;
2639
+ }
2640
+
2430
2641
  // Run slash-command router
2431
2642
  match handle_slash_command(&mut session, &query_str).await {
2432
2643
  Some(SlashResult::Handled) => continue,
@@ -2503,8 +2714,7 @@ async fn run_interactive_chat() -> Result<()> {
2503
2714
  print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
2504
2715
  let _ = io::stdout().flush();
2505
2716
 
2506
- let mut system_instruction = format!(
2507
- "You are Mint, a cute and helpful AI assistant. You speak in a polite, friendly, and sweet Thai tone (using \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
2717
+ let mut system_instruction = "You are Mint, a cute and helpful AI assistant. You speak in a polite, friendly, and sweet Thai tone (using \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
2508
2718
  You are running inside the Mint CLI interactive chat. \
2509
2719
  You have access to native system actions to help the user! If the user asks you to open a website, launch an app, read a file, list a folder, run code, run tests, or execute a local shell command, you can execute these actions by writing a special block at the very end of your response: \
2510
2720
  `[ACTION: <command> <arguments>]` \
@@ -2515,15 +2725,14 @@ async fn run_interactive_chat() -> Result<()> {
2515
2725
  - `[ACTION: read-folder <path>]` to list files/folders in a directory. \
2516
2726
  - `[ACTION: run-shell <command>]` to run a non-destructive local shell command after approval. \
2517
2727
  You may discuss mature personal topics at a non-explicit level, including adult relationships, sex education, intimacy, emotions, and feelings. Keep the tone respectful and supportive, avoid graphic sexual detail, and do not engage with sexual content involving minors, coercion, exploitation, or sexual violence. \
2518
- Write the action block on a single line at the very end of your response."
2519
- );
2520
- if let Ok(memory) = MemoryStore::open_default() {
2521
- if let Ok(Some(name)) = memory.get_profile("name") {
2522
- system_instruction.push_str(&format!(
2523
- "\nThe user's name is {}. Refer to them by their name when appropriate.",
2524
- name
2525
- ));
2526
- }
2728
+ Write the action block on a single line at the very end of your response.".to_string();
2729
+ if let Ok(memory) = MemoryStore::open_default()
2730
+ && let Ok(Some(name)) = memory.get_profile("name")
2731
+ {
2732
+ system_instruction.push_str(&format!(
2733
+ "\nThe user's name is {}. Refer to them by their name when appropriate.",
2734
+ name
2735
+ ));
2527
2736
  }
2528
2737
 
2529
2738
  let image_uri = session.pending_image.take();
@@ -2542,6 +2751,7 @@ async fn run_interactive_chat() -> Result<()> {
2542
2751
  audio_data_uri: None,
2543
2752
  document_attachment: None,
2544
2753
  workspace_path: None,
2754
+ agent_id: None,
2545
2755
  },
2546
2756
  |chunk| {
2547
2757
  if first_chunk {
@@ -2621,9 +2831,10 @@ async fn run_interactive_chat() -> Result<()> {
2621
2831
 
2622
2832
  fn print_exit_message(session: &InteractiveSession) {
2623
2833
  println!("\n{MINT}──────────────── Mint session closed ────────────────{RESET}");
2834
+ let clean_provider = format_provider_display_name(&session.config.ai_provider, &session.config);
2624
2835
  println!(
2625
2836
  "{DIM}Provider:{RESET} {} {DIM}• Model:{RESET} {}",
2626
- session.config.ai_provider,
2837
+ clean_provider,
2627
2838
  active_model(&session.config.ai_provider, &session.config)
2628
2839
  );
2629
2840
  println!(
@@ -2634,52 +2845,140 @@ fn print_exit_message(session: &InteractiveSession) {
2634
2845
  println!("{MINT}See you next time.{RESET}\n");
2635
2846
  }
2636
2847
 
2848
+ fn format_provider_display_name(provider: &str, config: &mint_core::MintConfig) -> String {
2849
+ if provider.starts_with("custom:") {
2850
+ let id = provider.strip_prefix("custom:").unwrap_or(provider);
2851
+ let cp = config.custom_providers.iter().find(|p| p.id == id);
2852
+ match cp {
2853
+ Some(p) if !p.display_name.is_empty() => format!("{} (Custom)", p.display_name),
2854
+ Some(p) => format!("{} (Custom)", p.id),
2855
+ None => format!("{} (Custom)", id),
2856
+ }
2857
+ } else {
2858
+ match provider {
2859
+ "gemini" => "Gemini".to_string(),
2860
+ "openai" => "OpenAI".to_string(),
2861
+ "openrouter" => "OpenRouter".to_string(),
2862
+ "deepseek" => "DeepSeek".to_string(),
2863
+ "anthropic" => "Claude".to_string(),
2864
+ "huggingface" => "HF".to_string(),
2865
+ "local_openai" => "Local".to_string(),
2866
+ "ollama" => "Ollama".to_string(),
2867
+ _ => {
2868
+ let mut chars = provider.chars();
2869
+ match chars.next() {
2870
+ None => String::new(),
2871
+ Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
2872
+ }
2873
+ }
2874
+ }
2875
+ }
2876
+ }
2877
+
2637
2878
  const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
2638
- ("/help", "Show help menu"),
2879
+ ("/cd", "Change active workspace directory"),
2880
+ ("/clear", "Clear conversation history"),
2881
+ ("/code", "Run in code-agent mode"),
2882
+ ("/exit", "Exit Mint CLI"),
2639
2883
  ("/fast", "Toggle fast mode (hide thinking traces)"),
2640
- ("/models", "List AI providers or switch active provider"),
2884
+ ("/help", "Show help menu"),
2885
+ ("/image", "Attach image from disk"),
2641
2886
  (
2642
2887
  "/image-provider",
2643
2888
  "List image gen providers or switch default provider",
2644
2889
  ),
2645
- ("/clear", "Clear conversation history"),
2646
- ("/cd", "Change active workspace directory"),
2647
- ("/image", "Attach image from disk"),
2648
- ("/paste", "Attach image from clipboard"),
2649
2890
  ("/learn", "Import persistent skill/instruction"),
2650
- ("/plugins", "List or generate plugins/skills"),
2651
- ("/memory", "Manage long-term memory store"),
2652
2891
  ("/mcp", "List configured MCP servers"),
2892
+ ("/memory", "Manage long-term memory store"),
2893
+ ("/models", "List AI providers or switch active provider"),
2894
+ (
2895
+ "/multi-agent",
2896
+ "Show or toggle Multi-Agent Collaboration system",
2897
+ ),
2898
+ ("/paste", "Attach image from clipboard"),
2899
+ ("/plugins", "List or generate plugins/skills"),
2900
+ ("/skill add", "Add or install global skill file or folder"),
2653
2901
  ("/stats", "Show session statistics"),
2654
- ("/exit", "Exit Mint CLI"),
2655
- ("/quit", "Exit Mint CLI"),
2656
- ("/code", "Run in code-agent mode"),
2657
2902
  ];
2658
2903
 
2904
+ fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
2905
+ std::fs::create_dir_all(&dst)?;
2906
+ for entry in std::fs::read_dir(src)? {
2907
+ let entry = entry?;
2908
+ let ty = entry.file_type()?;
2909
+ if ty.is_dir() {
2910
+ copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
2911
+ } else {
2912
+ std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
2913
+ }
2914
+ }
2915
+ Ok(())
2916
+ }
2917
+
2918
+ fn load_all_available_skills(current_dir: &Path) -> Vec<mint_core::LearnedSkill> {
2919
+ let mut skills = Vec::new();
2920
+ if let Ok(memory) = MemoryStore::open_default() {
2921
+ if let Ok(s) = memory.learned_skills(100) {
2922
+ skills = s;
2923
+ }
2924
+ }
2925
+ if let Some(home) = dirs::home_dir() {
2926
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
2927
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
2928
+ }
2929
+ let workspace_skills_path1 = current_dir.join(".agents").join("skills");
2930
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
2931
+ let workspace_skills_path2 = current_dir.join("skills");
2932
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
2933
+
2934
+ let mut unique_skills = std::collections::BTreeMap::new();
2935
+ for skill in skills {
2936
+ unique_skills.insert(skill.name.clone(), skill);
2937
+ }
2938
+ unique_skills.into_values().collect()
2939
+ }
2940
+
2659
2941
  fn draw_input_box(
2660
- input: &str,
2942
+ input_chars: &[char],
2943
+ cursor_pos: usize,
2661
2944
  placeholder: &str,
2662
2945
  model: &str,
2663
2946
  path_str: &str,
2664
2947
  tab_base_input: Option<&str>,
2665
2948
  tab_index: Option<usize>,
2666
- ) -> usize {
2949
+ current_dir: &Path,
2950
+ ) -> (usize, usize) {
2667
2951
  let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
2668
2952
  let width = term_width as usize;
2669
2953
  let prefix = "› ";
2670
2954
  let input_width = width.saturating_sub(2);
2671
2955
  let content_max_len = input_width.saturating_sub(prefix.chars().count());
2672
2956
 
2673
- let display_str = if input.is_empty() {
2957
+ let mut start_idx = 0;
2958
+ if input_chars.len() > content_max_len && cursor_pos >= content_max_len {
2959
+ start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
2960
+ if start_idx + content_max_len > input_chars.len() {
2961
+ start_idx = input_chars.len() - content_max_len;
2962
+ }
2963
+ }
2964
+
2965
+ let slice_chars = if input_chars.len() > content_max_len {
2966
+ &input_chars[start_idx..std::cmp::min(start_idx + content_max_len, input_chars.len())]
2967
+ } else {
2968
+ input_chars
2969
+ };
2970
+ let displayed_input: String = slice_chars.iter().collect();
2971
+
2972
+ let display_str = if displayed_input.is_empty() {
2674
2973
  format!("{DIM}{}\x1b[39m", placeholder)
2675
2974
  } else {
2676
- format_placeholders(input)
2975
+ format_placeholders(&displayed_input)
2677
2976
  };
2678
2977
 
2679
- let visible_len = if input.is_empty() {
2978
+ let visible_len = if displayed_input.is_empty() {
2680
2979
  placeholder.chars().count()
2681
2980
  } else {
2682
- string_visual_width(input)
2981
+ string_visual_width(&displayed_input)
2683
2982
  };
2684
2983
 
2685
2984
  let pad_len = content_max_len.saturating_sub(visible_len);
@@ -2707,7 +3006,8 @@ fn draw_input_box(
2707
3006
  );
2708
3007
 
2709
3008
  // Compute and draw suggestions
2710
- let search_query = tab_base_input.unwrap_or(input);
3009
+ let raw_input: String = input_chars.iter().collect();
3010
+ let search_query = tab_base_input.unwrap_or(&raw_input);
2711
3011
  let mut match_count = 0;
2712
3012
  if search_query.starts_with('/') {
2713
3013
  let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
@@ -2716,11 +3016,22 @@ fn draw_input_box(
2716
3016
  .collect();
2717
3017
 
2718
3018
  if !matches.is_empty() {
2719
- match_count = matches.len();
2720
- println!();
2721
- println!(" {BLUE}Suggestions{RESET}");
3019
+ let total_pages = matches.len().div_ceil(5);
2722
3020
  let highlight_idx = tab_index.map(|idx| idx % matches.len());
2723
- for (i, (cmd, desc)) in matches.iter().enumerate() {
3021
+ let selected_idx = highlight_idx.unwrap_or(0);
3022
+ let current_page = selected_idx / 5;
3023
+ let s_start_idx = current_page * 5;
3024
+ let s_end_idx = std::cmp::min(s_start_idx + 5, matches.len());
3025
+ match_count = s_end_idx - s_start_idx;
3026
+
3027
+ println!();
3028
+ println!(
3029
+ " {BLUE}Suggestions ({}/{}){RESET}",
3030
+ current_page + 1,
3031
+ total_pages
3032
+ );
3033
+ for i in s_start_idx..s_end_idx {
3034
+ let (cmd, desc) = matches[i];
2724
3035
  if Some(i) == highlight_idx {
2725
3036
  println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
2726
3037
  } else {
@@ -2728,14 +3039,86 @@ fn draw_input_box(
2728
3039
  }
2729
3040
  }
2730
3041
  }
3042
+ } else if search_query.starts_with('$') {
3043
+ // Parse skill name (excluding arguments)
3044
+ let skill_query = search_query
3045
+ .split_whitespace()
3046
+ .next()
3047
+ .unwrap_or(search_query);
3048
+ let prefix = &skill_query[1..].to_lowercase();
3049
+ let skills = load_all_available_skills(current_dir);
3050
+ let matches: Vec<_> = skills
3051
+ .iter()
3052
+ .filter(|skill| skill.name.to_lowercase().starts_with(prefix))
3053
+ .collect();
3054
+
3055
+ if !matches.is_empty() {
3056
+ let total_pages = matches.len().div_ceil(5);
3057
+ let highlight_idx = tab_index.map(|idx| idx % matches.len());
3058
+ let selected_idx = highlight_idx.unwrap_or(0);
3059
+ let current_page = selected_idx / 5;
3060
+ let s_start_idx = current_page * 5;
3061
+ let s_end_idx = std::cmp::min(s_start_idx + 5, matches.len());
3062
+ match_count = s_end_idx - s_start_idx;
3063
+
3064
+ println!();
3065
+ println!(
3066
+ " {BLUE}Suggestions ({}/{}){RESET}",
3067
+ current_page + 1,
3068
+ total_pages
3069
+ );
3070
+ for i in s_start_idx..s_end_idx {
3071
+ let skill = matches[i];
3072
+ let desc = skill
3073
+ .description
3074
+ .as_deref()
3075
+ .unwrap_or("No description provided");
3076
+ let max_desc_len = width.saturating_sub(35);
3077
+ let truncated_desc = if desc.chars().count() > max_desc_len {
3078
+ let mut s: String = desc.chars().take(max_desc_len.saturating_sub(3)).collect();
3079
+ s.push_str("...");
3080
+ s
3081
+ } else {
3082
+ desc.to_string()
3083
+ };
3084
+
3085
+ if Some(i) == highlight_idx {
3086
+ println!(
3087
+ " {BLUE}▶ ${:<20}{RESET} {MINT}[Skill]{RESET} {DIM}{}{RESET}",
3088
+ skill.name, truncated_desc
3089
+ );
3090
+ } else {
3091
+ println!(
3092
+ " {DIM}${:<20} [Skill] {}{RESET}",
3093
+ skill.name, truncated_desc
3094
+ );
3095
+ }
3096
+ }
3097
+ }
2731
3098
  }
2732
3099
 
2733
3100
  let _ = io::stdout().flush();
2734
- match_count
3101
+ (match_count, start_idx)
2735
3102
  }
2736
3103
 
2737
3104
  fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
2738
- let visual_cursor_pos: usize = input_chars[..cursor_pos]
3105
+ let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
3106
+ let width = term_width as usize;
3107
+ let prefix = "› ";
3108
+ let input_width = width.saturating_sub(2);
3109
+ let content_max_len = input_width.saturating_sub(prefix.chars().count());
3110
+
3111
+ let mut start_idx = 0;
3112
+ if input_chars.len() > content_max_len && cursor_pos >= content_max_len {
3113
+ start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
3114
+ if start_idx + content_max_len > input_chars.len() {
3115
+ start_idx = input_chars.len() - content_max_len;
3116
+ }
3117
+ }
3118
+
3119
+ let cursor_pos_clamped = std::cmp::min(cursor_pos, input_chars.len());
3120
+ let start_idx_clamped = std::cmp::min(start_idx, cursor_pos_clamped);
3121
+ let visual_cursor_pos: usize = input_chars[start_idx_clamped..cursor_pos_clamped]
2739
3122
  .iter()
2740
3123
  .copied()
2741
3124
  .map(char_visual_width)
@@ -2764,16 +3147,18 @@ fn redraw_input_box(
2764
3147
  path_str: &str,
2765
3148
  tab_base_input: Option<&str>,
2766
3149
  tab_index: Option<usize>,
3150
+ current_dir: &Path,
2767
3151
  ) {
2768
3152
  clear_input_box();
2769
- let input: String = input_chars.iter().collect();
2770
- let match_count = draw_input_box(
2771
- &input,
3153
+ let (match_count, _) = draw_input_box(
3154
+ input_chars,
3155
+ cursor_pos,
2772
3156
  placeholder,
2773
3157
  model,
2774
3158
  path_str,
2775
3159
  tab_base_input,
2776
3160
  tab_index,
3161
+ current_dir,
2777
3162
  );
2778
3163
  position_input_cursor(input_chars, cursor_pos, match_count);
2779
3164
  let _ = io::stdout().flush();
@@ -2783,6 +3168,7 @@ fn read_line_interactive(
2783
3168
  _provider: &str,
2784
3169
  model: &str,
2785
3170
  path_str: &str,
3171
+ current_dir: &Path,
2786
3172
  ) -> io::Result<Option<InteractiveInput>> {
2787
3173
  use crossterm::event::{self, Event, KeyCode};
2788
3174
  use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
@@ -2799,7 +3185,16 @@ fn read_line_interactive(
2799
3185
  let mut tab_base_input: Option<String> = None;
2800
3186
  let mut tab_index: Option<usize> = None;
2801
3187
 
2802
- let match_count = draw_input_box("", placeholder, model, path_str, None, None);
3188
+ let (match_count, _) = draw_input_box(
3189
+ &input_chars,
3190
+ cursor_pos,
3191
+ placeholder,
3192
+ model,
3193
+ path_str,
3194
+ None,
3195
+ None,
3196
+ current_dir,
3197
+ );
2803
3198
  position_input_cursor(&input_chars, cursor_pos, match_count);
2804
3199
  let _ = io::stdout().flush();
2805
3200
 
@@ -2839,70 +3234,177 @@ fn read_line_interactive(
2839
3234
  path_str,
2840
3235
  None,
2841
3236
  None,
3237
+ current_dir,
2842
3238
  );
2843
3239
  enable_raw_mode()?;
2844
3240
  last_paste_time = Some(std::time::Instant::now());
2845
3241
  continue;
2846
3242
  }
2847
- if let Event::Key(key_event) = ev {
2848
- if key_event.kind == event::KeyEventKind::Press {
2849
- let is_ctrl_d = matches!(key_event.code, KeyCode::Char('d'))
2850
- && key_event
3243
+ if let Event::Key(key_event) = ev
3244
+ && key_event.kind == event::KeyEventKind::Press
3245
+ {
3246
+ let is_ctrl_d = matches!(key_event.code, KeyCode::Char('d'))
3247
+ && key_event
3248
+ .modifiers
3249
+ .contains(crossterm::event::KeyModifiers::CONTROL);
3250
+ if ctrl_d_pressed && !is_ctrl_d {
3251
+ ctrl_d_pressed = false;
3252
+ disable_raw_mode()?;
3253
+ print!("\r\x1b[3B\r\x1b[2K\x1b[3A");
3254
+ print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
3255
+ let _ = io::stdout().flush();
3256
+ enable_raw_mode()?;
3257
+ }
3258
+
3259
+ // Reset tab autocomplete state if any key other than Tab, Up, or Down is pressed
3260
+ if key_event.code != KeyCode::Tab
3261
+ && key_event.code != KeyCode::Up
3262
+ && key_event.code != KeyCode::Down
3263
+ {
3264
+ tab_base_input = None;
3265
+ tab_index = None;
3266
+ }
3267
+
3268
+ match key_event.code {
3269
+ KeyCode::Char('d')
3270
+ if key_event
3271
+ .modifiers
3272
+ .contains(crossterm::event::KeyModifiers::CONTROL) =>
3273
+ {
3274
+ if ctrl_d_pressed {
3275
+ disable_raw_mode()?;
3276
+ clear_input_box();
3277
+ let _ = io::stdout().flush();
3278
+ break None;
3279
+ } else {
3280
+ ctrl_d_pressed = true;
3281
+ disable_raw_mode()?;
3282
+ print!(
3283
+ "\r\x1b[3B\r\x1b[2K{WARN}Press Ctrl+D again to exit{RESET}\x1b[3A"
3284
+ );
3285
+ print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
3286
+ let _ = io::stdout().flush();
3287
+ enable_raw_mode()?;
3288
+ }
3289
+ }
3290
+ KeyCode::Char('v')
3291
+ if key_event
2851
3292
  .modifiers
2852
- .contains(crossterm::event::KeyModifiers::CONTROL);
2853
- if ctrl_d_pressed && !is_ctrl_d {
2854
- ctrl_d_pressed = false;
3293
+ .contains(crossterm::event::KeyModifiers::CONTROL) =>
3294
+ {
3295
+ if let Ok(Some(uri)) = image::read_clipboard_image() {
3296
+ if let Some(ref mut current) = pasted_image {
3297
+ current.push(' ');
3298
+ current.push_str(&uri);
3299
+ } else {
3300
+ pasted_image = Some(uri);
3301
+ }
3302
+ insert_image_placeholder(&mut input_chars, &mut cursor_pos);
3303
+
3304
+ disable_raw_mode()?;
3305
+ redraw_input_box(
3306
+ &input_chars,
3307
+ cursor_pos,
3308
+ placeholder,
3309
+ model,
3310
+ path_str,
3311
+ None,
3312
+ None,
3313
+ current_dir,
3314
+ );
3315
+ enable_raw_mode()?;
3316
+ }
3317
+ }
3318
+ KeyCode::Char(c) if input_chars.len() < 10000 => {
3319
+ input_chars.insert(cursor_pos, c);
3320
+ cursor_pos += 1;
3321
+
2855
3322
  disable_raw_mode()?;
2856
- print!("\r\x1b[3B\r\x1b[2K\x1b[3A");
2857
- print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
2858
- let _ = io::stdout().flush();
3323
+ redraw_input_box(
3324
+ &input_chars,
3325
+ cursor_pos,
3326
+ placeholder,
3327
+ model,
3328
+ path_str,
3329
+ None,
3330
+ None,
3331
+ current_dir,
3332
+ );
2859
3333
  enable_raw_mode()?;
2860
3334
  }
3335
+ KeyCode::Backspace if cursor_pos > 0 => {
3336
+ cursor_pos -= 1;
3337
+ input_chars.remove(cursor_pos);
2861
3338
 
2862
- // Reset tab autocomplete state if any key other than Tab, Up, or Down is pressed
2863
- if key_event.code != KeyCode::Tab
2864
- && key_event.code != KeyCode::Up
2865
- && key_event.code != KeyCode::Down
2866
- {
2867
- tab_base_input = None;
2868
- tab_index = None;
3339
+ disable_raw_mode()?;
3340
+ redraw_input_box(
3341
+ &input_chars,
3342
+ cursor_pos,
3343
+ placeholder,
3344
+ model,
3345
+ path_str,
3346
+ None,
3347
+ None,
3348
+ current_dir,
3349
+ );
3350
+ enable_raw_mode()?;
2869
3351
  }
3352
+ KeyCode::Tab => {
3353
+ let base = match &tab_base_input {
3354
+ Some(b) => b.clone(),
3355
+ None => {
3356
+ let current_str: String = input_chars.iter().collect();
3357
+ tab_base_input = Some(current_str.clone());
3358
+ current_str
3359
+ }
3360
+ };
3361
+
3362
+ if base.starts_with('/') {
3363
+ let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
3364
+ .iter()
3365
+ .filter(|(cmd, _)| cmd.starts_with(&base))
3366
+ .collect();
3367
+
3368
+ if !matches.is_empty() {
3369
+ let idx = tab_index.unwrap_or(0) % matches.len();
3370
+ let completed = format!("{} ", matches[idx].0);
3371
+ input_chars = completed.chars().collect();
3372
+ cursor_pos = input_chars.len();
3373
+
3374
+ // Highlight currently completed item in suggestions
3375
+ let current_highlight = Some(idx);
3376
+ tab_index = Some(idx + 1);
2870
3377
 
2871
- match key_event.code {
2872
- KeyCode::Char('d')
2873
- if key_event
2874
- .modifiers
2875
- .contains(crossterm::event::KeyModifiers::CONTROL) =>
2876
- {
2877
- if ctrl_d_pressed {
2878
- disable_raw_mode()?;
2879
- clear_input_box();
2880
- let _ = io::stdout().flush();
2881
- break None;
2882
- } else {
2883
- ctrl_d_pressed = true;
2884
3378
  disable_raw_mode()?;
2885
- print!(
2886
- "\r\x1b[3B\r\x1b[2K{WARN}Press Ctrl+D again to exit{RESET}\x1b[3A"
3379
+ redraw_input_box(
3380
+ &input_chars,
3381
+ cursor_pos,
3382
+ placeholder,
3383
+ model,
3384
+ path_str,
3385
+ Some(&base),
3386
+ current_highlight,
3387
+ current_dir,
2887
3388
  );
2888
- print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
2889
- let _ = io::stdout().flush();
2890
3389
  enable_raw_mode()?;
2891
3390
  }
2892
- }
2893
- KeyCode::Char('v')
2894
- if key_event
2895
- .modifiers
2896
- .contains(crossterm::event::KeyModifiers::CONTROL) =>
2897
- {
2898
- if let Ok(Some(uri)) = image::read_clipboard_image() {
2899
- if let Some(ref mut current) = pasted_image {
2900
- current.push(' ');
2901
- current.push_str(&uri);
2902
- } else {
2903
- pasted_image = Some(uri);
2904
- }
2905
- insert_image_placeholder(&mut input_chars, &mut cursor_pos);
3391
+ } else if base.starts_with('$') {
3392
+ let skill_query = base.split_whitespace().next().unwrap_or(&base);
3393
+ let prefix = &skill_query[1..].to_lowercase();
3394
+ let skills = load_all_available_skills(current_dir);
3395
+ let matches: Vec<_> = skills
3396
+ .iter()
3397
+ .filter(|s| s.name.to_lowercase().starts_with(prefix))
3398
+ .collect();
3399
+
3400
+ if !matches.is_empty() {
3401
+ let idx = tab_index.unwrap_or(0) % matches.len();
3402
+ let completed = format!("${} ", matches[idx].name);
3403
+ input_chars = completed.chars().collect();
3404
+ cursor_pos = input_chars.len();
3405
+
3406
+ let current_highlight = Some(idx);
3407
+ tab_index = Some(idx + 1);
2906
3408
 
2907
3409
  disable_raw_mode()?;
2908
3410
  redraw_input_box(
@@ -2911,21 +3413,39 @@ fn read_line_interactive(
2911
3413
  placeholder,
2912
3414
  model,
2913
3415
  path_str,
2914
- None,
2915
- None,
3416
+ Some(&base),
3417
+ current_highlight,
3418
+ current_dir,
2916
3419
  );
2917
3420
  enable_raw_mode()?;
2918
3421
  }
2919
3422
  }
2920
- 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();
3423
+ }
3424
+ KeyCode::Down => {
3425
+ let base = match &tab_base_input {
3426
+ Some(b) => b.clone(),
3427
+ None => {
3428
+ let current_str: String = input_chars.iter().collect();
3429
+ tab_base_input = Some(current_str.clone());
3430
+ current_str
3431
+ }
3432
+ };
2925
3433
 
2926
- if current_visual_width < max_width {
2927
- input_chars.insert(cursor_pos, c);
2928
- cursor_pos += 1;
3434
+ if base.starts_with('/') {
3435
+ let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
3436
+ .iter()
3437
+ .filter(|(cmd, _)| cmd.starts_with(&base))
3438
+ .collect();
3439
+
3440
+ if !matches.is_empty() {
3441
+ let new_idx = match tab_index {
3442
+ Some(idx) => (idx + 1) % matches.len(),
3443
+ None => 0,
3444
+ };
3445
+ tab_index = Some(new_idx);
3446
+ let completed = format!("{} ", matches[new_idx].0);
3447
+ input_chars = completed.chars().collect();
3448
+ cursor_pos = input_chars.len();
2929
3449
 
2930
3450
  disable_raw_mode()?;
2931
3451
  redraw_input_box(
@@ -2934,16 +3454,30 @@ fn read_line_interactive(
2934
3454
  placeholder,
2935
3455
  model,
2936
3456
  path_str,
2937
- None,
2938
- None,
3457
+ Some(&base),
3458
+ Some(new_idx),
3459
+ current_dir,
2939
3460
  );
2940
3461
  enable_raw_mode()?;
2941
3462
  }
2942
- }
2943
- KeyCode::Backspace => {
2944
- if cursor_pos > 0 {
2945
- cursor_pos -= 1;
2946
- input_chars.remove(cursor_pos);
3463
+ } else if base.starts_with('$') {
3464
+ let skill_query = base.split_whitespace().next().unwrap_or(&base);
3465
+ let prefix = &skill_query[1..].to_lowercase();
3466
+ let skills = load_all_available_skills(current_dir);
3467
+ let matches: Vec<_> = skills
3468
+ .iter()
3469
+ .filter(|s| s.name.to_lowercase().starts_with(prefix))
3470
+ .collect();
3471
+
3472
+ if !matches.is_empty() {
3473
+ let new_idx = match tab_index {
3474
+ Some(idx) => (idx + 1) % matches.len(),
3475
+ None => 0,
3476
+ };
3477
+ tab_index = Some(new_idx);
3478
+ let completed = format!("${} ", matches[new_idx].name);
3479
+ input_chars = completed.chars().collect();
3480
+ cursor_pos = input_chars.len();
2947
3481
 
2948
3482
  disable_raw_mode()?;
2949
3483
  redraw_input_box(
@@ -2952,205 +3486,171 @@ fn read_line_interactive(
2952
3486
  placeholder,
2953
3487
  model,
2954
3488
  path_str,
2955
- None,
2956
- None,
3489
+ Some(&base),
3490
+ Some(new_idx),
3491
+ current_dir,
2957
3492
  );
2958
3493
  enable_raw_mode()?;
2959
3494
  }
2960
3495
  }
2961
- KeyCode::Tab => {
2962
- let base = match &tab_base_input {
2963
- Some(b) => b.clone(),
2964
- None => {
2965
- let current_str: String = input_chars.iter().collect();
2966
- tab_base_input = Some(current_str.clone());
2967
- current_str
2968
- }
2969
- };
2970
-
2971
- if base.starts_with('/') {
2972
- let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
2973
- .iter()
2974
- .filter(|(cmd, _)| cmd.starts_with(&base))
2975
- .collect();
2976
-
2977
- if !matches.is_empty() {
2978
- let idx = tab_index.unwrap_or(0) % matches.len();
2979
- let completed = format!("{} ", matches[idx].0);
2980
- input_chars = completed.chars().collect();
2981
- cursor_pos = input_chars.len();
2982
-
2983
- // Highlight currently completed item in suggestions
2984
- let current_highlight = Some(idx);
2985
- tab_index = Some(idx + 1);
2986
-
2987
- disable_raw_mode()?;
2988
- redraw_input_box(
2989
- &input_chars,
2990
- cursor_pos,
2991
- placeholder,
2992
- model,
2993
- path_str,
2994
- Some(&base),
2995
- current_highlight,
2996
- );
2997
- enable_raw_mode()?;
2998
- }
3496
+ }
3497
+ KeyCode::Up => {
3498
+ let base = match &tab_base_input {
3499
+ Some(b) => b.clone(),
3500
+ None => {
3501
+ let current_str: String = input_chars.iter().collect();
3502
+ tab_base_input = Some(current_str.clone());
3503
+ current_str
2999
3504
  }
3000
- }
3001
- KeyCode::Down => {
3002
- let base = match &tab_base_input {
3003
- Some(b) => b.clone(),
3004
- None => {
3005
- let current_str: String = input_chars.iter().collect();
3006
- tab_base_input = Some(current_str.clone());
3007
- current_str
3008
- }
3009
- };
3505
+ };
3010
3506
 
3011
- if base.starts_with('/') {
3012
- let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
3013
- .iter()
3014
- .filter(|(cmd, _)| cmd.starts_with(&base))
3015
- .collect();
3016
-
3017
- if !matches.is_empty() {
3018
- let new_idx = match tab_index {
3019
- Some(idx) => (idx + 1) % matches.len(),
3020
- None => 0,
3021
- };
3022
- tab_index = Some(new_idx);
3023
- let completed = format!("{} ", matches[new_idx].0);
3024
- input_chars = completed.chars().collect();
3025
- cursor_pos = input_chars.len();
3026
-
3027
- disable_raw_mode()?;
3028
- redraw_input_box(
3029
- &input_chars,
3030
- cursor_pos,
3031
- placeholder,
3032
- model,
3033
- path_str,
3034
- Some(&base),
3035
- Some(new_idx),
3036
- );
3037
- enable_raw_mode()?;
3038
- }
3039
- }
3040
- }
3041
- KeyCode::Up => {
3042
- let base = match &tab_base_input {
3043
- Some(b) => b.clone(),
3044
- None => {
3045
- let current_str: String = input_chars.iter().collect();
3046
- tab_base_input = Some(current_str.clone());
3047
- current_str
3048
- }
3049
- };
3507
+ if base.starts_with('/') {
3508
+ let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
3509
+ .iter()
3510
+ .filter(|(cmd, _)| cmd.starts_with(&base))
3511
+ .collect();
3512
+
3513
+ if !matches.is_empty() {
3514
+ let new_idx = match tab_index {
3515
+ Some(idx) => {
3516
+ if idx == 0 {
3517
+ matches.len() - 1
3518
+ } else {
3519
+ idx - 1
3520
+ }
3521
+ }
3522
+ None => matches.len() - 1,
3523
+ };
3524
+ tab_index = Some(new_idx);
3525
+ let completed = format!("{} ", matches[new_idx].0);
3526
+ input_chars = completed.chars().collect();
3527
+ cursor_pos = input_chars.len();
3050
3528
 
3051
- if base.starts_with('/') {
3052
- let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
3053
- .iter()
3054
- .filter(|(cmd, _)| cmd.starts_with(&base))
3055
- .collect();
3056
-
3057
- if !matches.is_empty() {
3058
- let new_idx = match tab_index {
3059
- Some(idx) => {
3060
- if idx == 0 {
3061
- matches.len() - 1
3062
- } else {
3063
- idx - 1
3064
- }
3529
+ disable_raw_mode()?;
3530
+ redraw_input_box(
3531
+ &input_chars,
3532
+ cursor_pos,
3533
+ placeholder,
3534
+ model,
3535
+ path_str,
3536
+ Some(&base),
3537
+ Some(new_idx),
3538
+ current_dir,
3539
+ );
3540
+ enable_raw_mode()?;
3541
+ }
3542
+ } else if base.starts_with('$') {
3543
+ let skill_query = base.split_whitespace().next().unwrap_or(&base);
3544
+ let prefix = &skill_query[1..].to_lowercase();
3545
+ let skills = load_all_available_skills(current_dir);
3546
+ let matches: Vec<_> = skills
3547
+ .iter()
3548
+ .filter(|s| s.name.to_lowercase().starts_with(prefix))
3549
+ .collect();
3550
+
3551
+ if !matches.is_empty() {
3552
+ let new_idx = match tab_index {
3553
+ Some(idx) => {
3554
+ if idx == 0 {
3555
+ matches.len() - 1
3556
+ } else {
3557
+ idx - 1
3065
3558
  }
3066
- None => matches.len() - 1,
3067
- };
3068
- tab_index = Some(new_idx);
3069
- let completed = format!("{} ", matches[new_idx].0);
3070
- input_chars = completed.chars().collect();
3071
- cursor_pos = input_chars.len();
3072
-
3073
- disable_raw_mode()?;
3074
- redraw_input_box(
3075
- &input_chars,
3076
- cursor_pos,
3077
- placeholder,
3078
- model,
3079
- path_str,
3080
- Some(&base),
3081
- Some(new_idx),
3082
- );
3083
- enable_raw_mode()?;
3084
- }
3559
+ }
3560
+ None => matches.len() - 1,
3561
+ };
3562
+ tab_index = Some(new_idx);
3563
+ let completed = format!("${} ", matches[new_idx].name);
3564
+ input_chars = completed.chars().collect();
3565
+ cursor_pos = input_chars.len();
3566
+
3567
+ disable_raw_mode()?;
3568
+ redraw_input_box(
3569
+ &input_chars,
3570
+ cursor_pos,
3571
+ placeholder,
3572
+ model,
3573
+ path_str,
3574
+ Some(&base),
3575
+ Some(new_idx),
3576
+ current_dir,
3577
+ );
3578
+ enable_raw_mode()?;
3085
3579
  }
3086
3580
  }
3087
- KeyCode::Left => {
3088
- while cursor_pos > 0 {
3089
- cursor_pos -= 1;
3090
- if cursor_pos == 0 || !is_thai_zero_width(input_chars[cursor_pos]) {
3091
- break;
3092
- }
3581
+ }
3582
+ KeyCode::Left => {
3583
+ while cursor_pos > 0 {
3584
+ cursor_pos -= 1;
3585
+ if cursor_pos == 0 || !is_thai_zero_width(input_chars[cursor_pos]) {
3586
+ break;
3093
3587
  }
3094
- 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();
3102
- enable_raw_mode()?;
3103
3588
  }
3104
- KeyCode::Right => {
3105
- while cursor_pos < input_chars.len() {
3106
- cursor_pos += 1;
3107
- if cursor_pos == input_chars.len()
3108
- || !is_thai_zero_width(input_chars[cursor_pos])
3109
- {
3110
- break;
3111
- }
3589
+ disable_raw_mode()?;
3590
+ redraw_input_box(
3591
+ &input_chars,
3592
+ cursor_pos,
3593
+ placeholder,
3594
+ model,
3595
+ path_str,
3596
+ None,
3597
+ None,
3598
+ current_dir,
3599
+ );
3600
+ enable_raw_mode()?;
3601
+ }
3602
+ KeyCode::Right => {
3603
+ while cursor_pos < input_chars.len() {
3604
+ cursor_pos += 1;
3605
+ if cursor_pos == input_chars.len()
3606
+ || !is_thai_zero_width(input_chars[cursor_pos])
3607
+ {
3608
+ break;
3112
3609
  }
3113
- 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();
3121
- enable_raw_mode()?;
3122
3610
  }
3123
- KeyCode::Enter => {
3124
- if let Some(time) = last_paste_time {
3125
- if time.elapsed() < std::time::Duration::from_millis(100) {
3126
- continue;
3127
- }
3128
- }
3129
- disable_raw_mode()?;
3130
- clear_input_box();
3131
- let input_str: String = input_chars.iter().collect();
3611
+ disable_raw_mode()?;
3612
+ redraw_input_box(
3613
+ &input_chars,
3614
+ cursor_pos,
3615
+ placeholder,
3616
+ model,
3617
+ path_str,
3618
+ None,
3619
+ None,
3620
+ current_dir,
3621
+ );
3622
+ enable_raw_mode()?;
3623
+ }
3624
+ KeyCode::Enter => {
3625
+ if let Some(time) = last_paste_time
3626
+ && time.elapsed() < std::time::Duration::from_millis(100)
3627
+ {
3628
+ continue;
3629
+ }
3630
+ disable_raw_mode()?;
3631
+ clear_input_box();
3632
+ let input_str: String = input_chars.iter().collect();
3132
3633
 
3133
- let mut expanded_str = input_str.clone();
3134
- for (placeholder_str, content) in &paste_contents {
3135
- expanded_str = expanded_str.replace(placeholder_str, content);
3136
- }
3634
+ let mut expanded_str = input_str.clone();
3635
+ for (placeholder_str, content) in &paste_contents {
3636
+ expanded_str = expanded_str.replace(placeholder_str, content);
3637
+ }
3137
3638
 
3138
- println!(" {BLUE}You ›{RESET} {}", expanded_str);
3139
- let _ = io::stdout().flush();
3639
+ println!(" {BLUE}You ›{RESET} {}", expanded_str);
3640
+ let _ = io::stdout().flush();
3140
3641
 
3141
- break Some(InteractiveInput {
3142
- text: expanded_str,
3143
- pasted_image,
3144
- });
3145
- }
3146
- KeyCode::Esc => {
3147
- disable_raw_mode()?;
3148
- clear_input_box();
3149
- let _ = io::stdout().flush();
3150
- break None;
3151
- }
3152
- _ => {}
3642
+ break Some(InteractiveInput {
3643
+ text: expanded_str,
3644
+ pasted_image,
3645
+ });
3646
+ }
3647
+ KeyCode::Esc => {
3648
+ disable_raw_mode()?;
3649
+ clear_input_box();
3650
+ let _ = io::stdout().flush();
3651
+ break None;
3153
3652
  }
3653
+ _ => {}
3154
3654
  }
3155
3655
  }
3156
3656
  }
@@ -3190,18 +3690,18 @@ fn format_placeholders(s: &str) -> String {
3190
3690
  while i < chars.len() {
3191
3691
  if chars[i] == '[' && i + 12 < chars.len() {
3192
3692
  let slice: String = chars[i..i + 13].iter().collect();
3193
- if slice == "[Pasted text #" {
3194
- if let Some(end_offset) = chars[i..].iter().position(|&c| c == ']') {
3195
- let end_idx = i + end_offset;
3196
- let inside: String = chars[i + 1..end_idx].iter().collect();
3197
- result.push('[');
3198
- result.push_str(BLUE);
3199
- result.push_str(&inside);
3200
- result.push_str("\x1b[39m");
3201
- result.push(']');
3202
- i = end_idx + 1;
3203
- continue;
3204
- }
3693
+ if slice == "[Pasted text #"
3694
+ && let Some(end_offset) = chars[i..].iter().position(|&c| c == ']')
3695
+ {
3696
+ let end_idx = i + end_offset;
3697
+ let inside: String = chars[i + 1..end_idx].iter().collect();
3698
+ result.push('[');
3699
+ result.push_str(BLUE);
3700
+ result.push_str(&inside);
3701
+ result.push_str("\x1b[39m");
3702
+ result.push(']');
3703
+ i = end_idx + 1;
3704
+ continue;
3205
3705
  }
3206
3706
  }
3207
3707
  result.push(chars[i]);
@@ -3232,6 +3732,22 @@ fn active_model<'a>(provider: &str, config: &'a mint_core::MintConfig) -> &'a st
3232
3732
  "huggingface" => &config.hf_model,
3233
3733
  "local_openai" => &config.local_model_name,
3234
3734
  "ollama" => &config.ollama_model,
3735
+ p if p.starts_with("custom:") => {
3736
+ if let Some(id) = p.strip_prefix("custom:")
3737
+ && let Some(selections) = config
3738
+ .extra
3739
+ .get("customModelSelections")
3740
+ .and_then(|v| v.as_object())
3741
+ && let Some(m) = selections.get(id).and_then(|v| v.as_str())
3742
+ {
3743
+ return m;
3744
+ }
3745
+ config
3746
+ .resolve_custom_provider(p)
3747
+ .and_then(|cp| cp.models.first())
3748
+ .map(|m| m.model_id.as_str())
3749
+ .unwrap_or("")
3750
+ }
3235
3751
  _ => &config.gemini_model,
3236
3752
  }
3237
3753
  }
@@ -3566,6 +4082,7 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
3566
4082
  audio_data_uri: None,
3567
4083
  document_attachment: None,
3568
4084
  workspace_path: None,
4085
+ agent_id: None,
3569
4086
  },
3570
4087
  )
3571
4088
  .await?;