@pheem49/mint 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +20 -6
  5. package/Release_Note.md +129 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/bin/mint +0 -0
  8. package/crates/mint-cli/Cargo.toml +1 -0
  9. package/crates/mint-cli/src/agent.rs +242 -54
  10. package/crates/mint-cli/src/main.rs +397 -74
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +64 -2
  13. package/crates/mint-core/src/browser.rs +456 -0
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +3 -0
  16. package/crates/mint-core/src/config.rs +130 -1
  17. package/crates/mint-core/src/lib.rs +19 -0
  18. package/crates/mint-core/src/orchestration.rs +317 -12
  19. package/crates/mint-core/src/skills.rs +113 -4
  20. package/crates/mint-core/src/tts.rs +3 -1
  21. package/crates/mint-core/src/workflows.rs +1 -22
  22. package/flatpak/com.pheem49.mint.desktop +9 -0
  23. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  24. package/flatpak/com.pheem49.mint.yaml +52 -0
  25. package/package.json +1 -1
  26. package/shared-modules/.github/workflows/ci.yml +120 -0
  27. package/shared-modules/.github/workflows/update.yaml +28 -0
  28. package/shared-modules/CODEOWNERS +23 -0
  29. package/shared-modules/README.md +79 -0
  30. package/shared-modules/SDL/README.md +3 -0
  31. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  32. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  33. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  34. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  35. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  36. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  37. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  38. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  39. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  40. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  41. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  42. package/shared-modules/SDL/sdl12-compat.json +22 -0
  43. package/shared-modules/SDL2/SDL2_image.json +27 -0
  44. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  45. package/shared-modules/cld2/CMakeLists.txt +157 -0
  46. package/shared-modules/cld2/cld2.json +24 -0
  47. package/shared-modules/clutter/clutter.json +72 -0
  48. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  49. package/shared-modules/glew/glew.json +36 -0
  50. package/shared-modules/glu/glu-9.json +12 -0
  51. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  52. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  53. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  54. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  55. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  56. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  57. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  58. package/shared-modules/gtk2/gtk2.json +120 -0
  59. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  60. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  61. package/shared-modules/gtk2/readme.md +10 -0
  62. package/shared-modules/intltool/intltool-0.51.json +20 -0
  63. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  64. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  65. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  66. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  67. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  68. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  69. package/shared-modules/lame/lame-3.99.5.json +32 -0
  70. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  71. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  72. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  73. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  74. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  75. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  76. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  77. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  78. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  79. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  80. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  81. package/shared-modules/libcanberra/libcanberra.json +44 -0
  82. package/shared-modules/libmad/libmad.json +19 -0
  83. package/shared-modules/libsecret/libsecret.json +23 -0
  84. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  85. package/shared-modules/libusb/libusb.json +27 -0
  86. package/shared-modules/linux-audio/dssi.json +31 -0
  87. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  88. package/shared-modules/linux-audio/fftw3f.json +42 -0
  89. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  90. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  91. package/shared-modules/linux-audio/ladspa.json +26 -0
  92. package/shared-modules/linux-audio/lash.json +35 -0
  93. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  94. package/shared-modules/linux-audio/liblo-static.json +22 -0
  95. package/shared-modules/linux-audio/liblo.json +17 -0
  96. package/shared-modules/linux-audio/lilv-static.json +94 -0
  97. package/shared-modules/linux-audio/lilv.json +82 -0
  98. package/shared-modules/linux-audio/lrdf.json +57 -0
  99. package/shared-modules/linux-audio/lv2.json +21 -0
  100. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  101. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  102. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  103. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  104. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  105. package/shared-modules/linux-audio/stk.json +28 -0
  106. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  107. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  108. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  109. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  110. package/shared-modules/lua5.3/lua.pc.in +8 -0
  111. package/shared-modules/lua5.3/luaconf.patch +47 -0
  112. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  113. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  114. package/shared-modules/lua5.4/lua.pc.in +8 -0
  115. package/shared-modules/lua5.4/luaconf.patch +34 -0
  116. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  117. package/shared-modules/luajit/luajit.json +30 -0
  118. package/shared-modules/lzo/lzo.json +27 -0
  119. package/shared-modules/mac/mac.json +18 -0
  120. package/shared-modules/physfs/physfs.json +26 -0
  121. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  122. package/shared-modules/pygame/cython-fixes.patch +34 -0
  123. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  124. package/shared-modules/pygame/glib.patch +25 -0
  125. package/shared-modules/pygame/pygame1-python2.json +157 -0
  126. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  127. package/shared-modules/pygtk/pygtk.json +117 -0
  128. package/shared-modules/pygtk/pygtk.patch +39 -0
  129. package/shared-modules/python2.7/python-2.7.json +48 -0
  130. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  131. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  132. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  133. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  134. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  135. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  136. package/shared-modules/update-checker.sh +50 -0
  137. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  138. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  139. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  140. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  141. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  142. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  143. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  144. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  145. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  146. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  147. package/shared-modules/vorbisgain/README.md +23 -0
  148. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  149. package/src/renderer/shared/agentProgress.ts +145 -0
  150. package/src/renderer/shared/calculator.ts +52 -0
  151. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  152. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  153. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  154. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  155. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  156. package/src/renderer/shared/constants/models.ts +91 -0
  157. package/src/renderer/shared/platform.ts +82 -0
  158. package/src/renderer/shared/types.ts +140 -0
  159. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  160. package/src/renderer/shared/utils/approval.ts +1 -0
  161. package/src/renderer/shared/utils/approval.tsx +64 -0
  162. package/src/renderer/shared/utils/markdown.tsx +347 -0
  163. package/src/renderer/shared/utils/providers.ts +36 -0
  164. package/src/renderer/shared/utils/speech.ts +216 -0
  165. package/src/renderer/shared/utils/ui.ts +147 -0
  166. package/src/renderer/src/agentProgress.ts +11 -39
  167. package/src/renderer/src/calculator.ts +3 -46
  168. package/src/renderer/src/components/ChatPanel.tsx +230 -1202
  169. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  170. package/src/renderer/src/components/MintDashboard.tsx +63 -171
  171. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  172. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  173. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  174. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  175. package/src/renderer/src/components/SettingsWindow.tsx +54 -12
  176. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  177. package/src/renderer/src/css/image-studio.css +6 -0
  178. package/src/renderer/src/css/styles.css +148 -11
  179. package/src/renderer/src/index.css +12 -1
  180. package/src/renderer/src/tauri.ts +169 -143
  181. package/src/renderer/src-web/agentProgress.ts +11 -39
  182. package/src/renderer/src-web/calculator.ts +3 -46
  183. package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
  184. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  185. package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
  186. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  187. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  188. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  189. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  190. package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
  191. package/src/renderer/src-web/css/image-studio.css +6 -0
  192. package/src/renderer/src-web/css/styles.css +184 -12
  193. package/src/renderer/src-web/index.css +12 -1
  194. package/src/renderer/src-web/tauri.ts +204 -104
  195. package/src-tauri/src/browser.rs +1 -141
  196. package/src-tauri/src/headless.rs +3 -2
  197. package/src-tauri/src/lib.rs +254 -59
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +57 -4
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -93,7 +93,7 @@ pub async fn orchestrate_chat(
93
93
  let mut resolved_request = request.clone();
94
94
  resolved_request.message = resolve_github_links(&request.message, config).await;
95
95
  let memory = MemoryStore::open_default()?;
96
- let enriched = enrich_request(&memory, &resolved_request)?;
96
+ let enriched = enrich_request(config, &memory, &resolved_request)?;
97
97
  let response = send_chat(config, &enriched).await?;
98
98
  memory.add_interaction_for_chat_with_fallback(
99
99
  request_chat_id(request),
@@ -122,7 +122,7 @@ where
122
122
  let mut resolved_request = request.clone();
123
123
  resolved_request.message = resolve_github_links(&request.message, config).await;
124
124
  let memory = MemoryStore::open_default()?;
125
- let enriched = enrich_request(&memory, &resolved_request)?;
125
+ let enriched = enrich_request(config, &memory, &resolved_request)?;
126
126
  let response = stream_chat(config, &enriched, on_chunk).await?;
127
127
  memory.add_interaction_for_chat_with_fallback(
128
128
  request_chat_id(request),
@@ -147,7 +147,7 @@ pub async fn orchestrate_chat_with_fallback(
147
147
  let mut resolved_request = request.clone();
148
148
  resolved_request.message = resolve_github_links(&request.message, config).await;
149
149
  let memory = MemoryStore::open_default()?;
150
- let enriched = enrich_request(&memory, &resolved_request)?;
150
+ let enriched = enrich_request(config, &memory, &resolved_request)?;
151
151
  let (response, fallback) = send_chat_with_fallback(config, &enriched).await?;
152
152
  memory.add_interaction_for_chat_with_fallback(
153
153
  request_chat_id(request),
@@ -176,7 +176,7 @@ where
176
176
  let mut resolved_request = request.clone();
177
177
  resolved_request.message = resolve_github_links(&request.message, config).await;
178
178
  let memory = MemoryStore::open_default()?;
179
- let enriched = enrich_request(&memory, &resolved_request)?;
179
+ let enriched = enrich_request(config, &memory, &resolved_request)?;
180
180
  let (response, fallback) = stream_chat_with_fallback(config, &enriched, on_chunk).await?;
181
181
  memory.add_interaction_for_chat_with_fallback(
182
182
  request_chat_id(request),
@@ -194,7 +194,11 @@ where
194
194
  Ok((response, fallback))
195
195
  }
196
196
 
197
- fn enrich_request(memory: &MemoryStore, request: &ChatRequest) -> Result<ChatRequest, MemoryError> {
197
+ fn enrich_request(
198
+ config: &MintConfig,
199
+ memory: &MemoryStore,
200
+ request: &ChatRequest,
201
+ ) -> Result<ChatRequest, MemoryError> {
198
202
  let mut interactions =
199
203
  memory.recent_interactions_for_chat(request_chat_id(request), CONTEXT_LIMIT)?;
200
204
  interactions.reverse();
@@ -230,6 +234,16 @@ fn enrich_request(memory: &MemoryStore, request: &ChatRequest) -> Result<ChatReq
230
234
  .to_owned();
231
235
  }
232
236
 
237
+ // Inject active AI model/provider context to system instructions
238
+ let active_model_info = format!(
239
+ "\n\n[Active Environment Context]\n\
240
+ You are running on: {}\n\
241
+ Using AI Model: {}\n",
242
+ config.ai_provider,
243
+ config.active_model()
244
+ );
245
+ enriched.system_instruction.push_str(&active_model_info);
246
+
233
247
  if !transcript.is_empty() {
234
248
  enriched.system_instruction = format!(
235
249
  "{}\n\nRecent conversation context:\n{}",
@@ -253,6 +267,16 @@ fn request_chat_id(request: &ChatRequest) -> &str {
253
267
 
254
268
  const MAX_STEPS: usize = 32;
255
269
  const MAX_OBSERVATION_BYTES: usize = 16_000;
270
+ fn is_port_9222_open() -> bool {
271
+ use std::net::TcpStream;
272
+ use std::time::Duration;
273
+ if let Ok(addr) = "127.0.0.1:9222".parse() {
274
+ TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok()
275
+ } else {
276
+ false
277
+ }
278
+ }
279
+
256
280
  pub fn build_system_prompt(config: &MintConfig) -> String {
257
281
  let mut allowed_actions = vec![
258
282
  "list_files",
@@ -284,6 +308,14 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
284
308
  "apply_patch",
285
309
  "write_file",
286
310
  ];
311
+
312
+ if is_port_9222_open() {
313
+ allowed_actions.push("browser_open");
314
+ allowed_actions.push("browser_click");
315
+ allowed_actions.push("browser_type");
316
+ allowed_actions.push("browser_read");
317
+ }
318
+
287
319
  allowed_actions.retain(|action| !config.disabled_tools.contains(&action.to_string()));
288
320
  allowed_actions.push("finish");
289
321
 
@@ -319,6 +351,19 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
319
351
  if allowed_actions.contains(&"web_search") {
320
352
  input_formats.push("- web_search: {\"query\":\"search terms\",\"limit\":5}");
321
353
  }
354
+ if allowed_actions.contains(&"browser_open") {
355
+ input_formats.push("- browser_open: {\"url\":\"https://example.com\"}");
356
+ }
357
+ if allowed_actions.contains(&"browser_click") {
358
+ input_formats.push("- browser_click: {\"selector\":\"button.submit-btn\"}");
359
+ }
360
+ if allowed_actions.contains(&"browser_type") {
361
+ input_formats
362
+ .push("- browser_type: {\"selector\":\"input.search-bar\", \"text\":\"search query\"}");
363
+ }
364
+ if allowed_actions.contains(&"browser_read") {
365
+ input_formats.push("- browser_read: {}");
366
+ }
322
367
  if allowed_actions.contains(&"memory_recall") {
323
368
  input_formats.push("- memory_recall: {\"query\":\"what did user say about X\"}");
324
369
  }
@@ -420,6 +465,20 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
420
465
  if allowed_actions.contains(&"web_search") {
421
466
  rules.push("7. Use web_search when the user asks to look something up online or needs current information.");
422
467
  }
468
+ if allowed_actions.contains(&"browser_open") {
469
+ rules.push("7a. Use browser_open to navigate the virtual browser to a URL.");
470
+ }
471
+ if allowed_actions.contains(&"browser_click") {
472
+ rules.push("7b. Use browser_click to click elements on the page.");
473
+ }
474
+ if allowed_actions.contains(&"browser_type") {
475
+ rules.push("7c. Use browser_type to enter text/input into form fields or search boxes.");
476
+ }
477
+ if allowed_actions.contains(&"browser_read") {
478
+ rules.push(
479
+ "7d. Use browser_read to read the text content of the active page to analyze it.",
480
+ );
481
+ }
423
482
  if allowed_actions.contains(&"memory_recall") {
424
483
  rules.push("8. Use memory_recall to search past interactions before asking the user to repeat context.");
425
484
  }
@@ -504,6 +563,10 @@ pub enum ApprovalOutcome {
504
563
  pub enum AgentProgress {
505
564
  Thinking {
506
565
  elapsed_secs: u64,
566
+ #[serde(skip_serializing_if = "Option::is_none")]
567
+ agent_name: Option<String>,
568
+ #[serde(skip_serializing_if = "Option::is_none")]
569
+ model_name: Option<String>,
507
570
  },
508
571
  Thought {
509
572
  thought: String,
@@ -590,6 +653,12 @@ struct AgentInput {
590
653
  title: String,
591
654
  #[serde(default)]
592
655
  status: String,
656
+ #[serde(default)]
657
+ url: String,
658
+ #[serde(default)]
659
+ selector: String,
660
+ #[serde(default)]
661
+ text: String,
593
662
  }
594
663
 
595
664
  #[derive(Debug, Deserialize, Serialize)]
@@ -607,12 +676,95 @@ where
607
676
  Ok(Option::<AgentInput>::deserialize(deserializer)?.unwrap_or_default())
608
677
  }
609
678
 
679
+ fn resolve_agent_config(
680
+ config: &MintConfig,
681
+ agent_id: Option<&str>,
682
+ trajectory: &[String],
683
+ ) -> (MintConfig, String, Option<String>, Option<String>) {
684
+ if !config.enable_agent_collaboration {
685
+ return (config.clone(), "".to_string(), None, None);
686
+ }
687
+
688
+ let enabled_agents: Vec<&crate::config::AgentConfig> = config.agents.iter().filter(|a| a.enabled).collect();
689
+ if enabled_agents.is_empty() {
690
+ return (config.clone(), "".to_string(), None, None);
691
+ }
692
+
693
+ let active_agent = if let Some(id) = agent_id {
694
+ enabled_agents.iter().find(|a| a.id == id).copied()
695
+ } else {
696
+ // Multi-Agent Pipeline Collaboration (Planner -> Coder -> Reviewer)
697
+ let plan_created = trajectory.iter().any(|step| step.contains("- Action: create_plan"));
698
+ if !plan_created {
699
+ if let Some(planner) = enabled_agents.iter().find(|a| a.id == "planner") {
700
+ Some(*planner)
701
+ } else {
702
+ enabled_agents.iter().find(|a| a.id == "coder").copied()
703
+ }
704
+ } else {
705
+ // Check if edits have been made to verify
706
+ let edits_made = trajectory.iter().any(|step| {
707
+ step.contains("- Action: write_file") || step.contains("- Action: apply_patch")
708
+ });
709
+ if edits_made {
710
+ if let Some(last_step) = trajectory.last() {
711
+ if last_step.contains("- Action: write_file") || last_step.contains("- Action: apply_patch") {
712
+ if let Some(reviewer) = enabled_agents.iter().find(|a| a.id == "reviewer") {
713
+ Some(*reviewer)
714
+ } else {
715
+ enabled_agents.iter().find(|a| a.id == "coder").copied()
716
+ }
717
+ } else {
718
+ enabled_agents.iter().find(|a| a.id == "coder").copied()
719
+ }
720
+ } else {
721
+ enabled_agents.iter().find(|a| a.id == "coder").copied()
722
+ }
723
+ } else {
724
+ enabled_agents.iter().find(|a| a.id == "coder").copied()
725
+ }
726
+ }
727
+ };
728
+
729
+ let Some(agent) = active_agent else {
730
+ return (config.clone(), "".to_string(), None, None);
731
+ };
732
+
733
+ let mut cfg_clone = config.clone();
734
+ cfg_clone.ai_provider = agent.provider.clone();
735
+ cfg_clone.gemini_model = agent.model.clone();
736
+ cfg_clone.openai_model = agent.model.clone();
737
+ cfg_clone.anthropic_model = agent.model.clone();
738
+ cfg_clone.openrouter_model = agent.model.clone();
739
+ cfg_clone.deepseek_model = agent.model.clone();
740
+ cfg_clone.hf_model = agent.model.clone();
741
+ cfg_clone.local_model_name = agent.model.clone();
742
+ cfg_clone.ollama_model = agent.model.clone();
743
+
744
+ if let Some(key) = &agent.api_key {
745
+ if !key.trim().is_empty() {
746
+ match agent.provider.as_str() {
747
+ "gemini" => cfg_clone.api_key = key.clone(),
748
+ "openai" => cfg_clone.openai_api_key = key.clone(),
749
+ "anthropic" => cfg_clone.anthropic_api_key = key.clone(),
750
+ "openrouter" => cfg_clone.openrouter_api_key = key.clone(),
751
+ "deepseek" => cfg_clone.deepseek_api_key = key.clone(),
752
+ "huggingface" => cfg_clone.hf_api_key = key.clone(),
753
+ _ => {}
754
+ }
755
+ }
756
+ }
757
+
758
+ (cfg_clone, agent.system_instruction.clone(), Some(agent.name.clone()), Some(agent.model.clone()))
759
+ }
760
+
610
761
  pub async fn orchestrate_agent_loop<Approve, Progress, Chunk>(
611
762
  config: &MintConfig,
612
763
  task: &str,
613
764
  root: &Path,
614
765
  image_data_uri: Option<String>,
615
766
  chat_id: Option<&str>,
767
+ agent_id: Option<&str>,
616
768
  fast_mode: bool,
617
769
  mut approve: Approve,
618
770
  mut progress: Progress,
@@ -632,7 +784,7 @@ where
632
784
  ))
633
785
  })?;
634
786
  let resolved_task = resolve_github_links(task, config).await;
635
- let skills = crate::skills::learned_skills_context().unwrap_or_default();
787
+ let skills = crate::skills::learned_skills_context(Some(&root)).unwrap_or_default();
636
788
  let mut observation = initial_observation(&resolved_task, &root, &skills);
637
789
  let mut pending_image = image_data_uri;
638
790
 
@@ -692,20 +844,39 @@ where
692
844
  let mut trajectory: Vec<String> = Vec::new();
693
845
 
694
846
  for step in 1..=MAX_STEPS {
847
+ let (active_config, agent_instruction, active_agent_name, active_model_name) = resolve_agent_config(config, agent_id, &trajectory);
848
+
695
849
  progress(AgentProgress::Thinking {
696
850
  elapsed_secs: started_at.elapsed().as_secs(),
851
+ agent_name: active_agent_name,
852
+ model_name: active_model_name.clone(),
697
853
  });
698
854
 
855
+ let mut active_system_prompt = system_prompt.clone();
856
+ if let Some(ref model_name) = active_model_name {
857
+ active_system_prompt.push_str(&format!(
858
+ "\n\n[Active Environment Context]\n\
859
+ You are running on: {}\n\
860
+ Using AI Model: {}\n",
861
+ active_config.ai_provider,
862
+ model_name
863
+ ));
864
+ }
865
+ if !agent_instruction.is_empty() {
866
+ active_system_prompt.push_str(&format!("\n\nYour Current Role & System Instructions:\n{}", agent_instruction));
867
+ }
868
+
699
869
  let (response, fallback) = send_chat_with_fallback(
700
- config,
870
+ &active_config,
701
871
  &ChatRequest {
702
872
  message: observation.clone(),
703
- system_instruction: system_prompt.clone(),
873
+ system_instruction: active_system_prompt.clone(),
704
874
  chat_id: Some(chat_id.to_owned()),
705
875
  image_data_uri: pending_image.take(),
706
876
  audio_data_uri: None,
707
877
  document_attachment: None,
708
878
  workspace_path: None,
879
+ agent_id: None,
709
880
  },
710
881
  )
711
882
  .await?;
@@ -720,7 +891,7 @@ where
720
891
  Ok(decision) => decision,
721
892
  Err(_) => {
722
893
  let (repaired, _) = send_chat_with_fallback(
723
- config,
894
+ &active_config,
724
895
  &ChatRequest {
725
896
  message: format!(
726
897
  "Your previous response was not valid Mint agent JSON.\n\
@@ -728,12 +899,13 @@ where
728
899
  Do not use markdown.\n\nPrevious response:\n{}",
729
900
  truncate(&response.text)
730
901
  ),
731
- system_instruction: system_prompt.clone(),
902
+ system_instruction: active_system_prompt.clone(),
732
903
  chat_id: Some(chat_id.to_owned()),
733
904
  image_data_uri: None,
734
905
  audio_data_uri: None,
735
906
  document_attachment: None,
736
907
  workspace_path: None,
908
+ agent_id: None,
737
909
  },
738
910
  )
739
911
  .await?;
@@ -1064,7 +1236,7 @@ where
1064
1236
  .collect::<Vec<_>>()
1065
1237
  .join("\n");
1066
1238
  Ok(format!(
1067
- "{formatted}\n\nNote: Web search succeeded using {provider} Search. In your finish summary, you MUST state that you found this information using the {provider} Search API (e.g. \"มิ้นท์ค้นหาข้อมูลนี้มาจาก {provider} Search นะคะ\")."
1239
+ "{formatted}\n\nNote: Web search succeeded using {provider} Search. In your finish summary, you MUST:\n1. Answer the user's question using the information above.\n2. Mention that you found this information via {provider} Search (e.g. \"I found this information using {provider} Search.\").\nDo NOT list sources manually — the UI will display them automatically."
1068
1240
  ))
1069
1241
  }
1070
1242
  }
@@ -1076,6 +1248,91 @@ where
1076
1248
  )),
1077
1249
  }
1078
1250
  }
1251
+ "browser_open" => {
1252
+ let url = if !input.url.is_empty() {
1253
+ &input.url
1254
+ } else if !input.path.is_empty() {
1255
+ &input.path
1256
+ } else {
1257
+ return Err(OrchestrationError::Agent(
1258
+ "browser_open requires 'url'".into(),
1259
+ ));
1260
+ };
1261
+ if crate::is_browser_running(config).await {
1262
+ let result = crate::browser::navigate(config, url)
1263
+ .await
1264
+ .map_err(|e| OrchestrationError::Agent(e))?;
1265
+ Ok(result)
1266
+ } else {
1267
+ let opened = if cfg!(target_os = "macos") {
1268
+ std::process::Command::new("open").arg(url).spawn().is_ok()
1269
+ } else if cfg!(target_os = "windows") {
1270
+ std::process::Command::new("cmd")
1271
+ .args(&["/C", "start", url])
1272
+ .spawn()
1273
+ .is_ok()
1274
+ } else {
1275
+ std::process::Command::new("xdg-open")
1276
+ .arg(url)
1277
+ .spawn()
1278
+ .is_ok()
1279
+ };
1280
+ if opened {
1281
+ Ok(format!(
1282
+ "Mint Auto is not active. Opened {url} in your default browser instead."
1283
+ ))
1284
+ } else {
1285
+ Err(OrchestrationError::Agent(
1286
+ "Failed to open URL in default browser.".into(),
1287
+ ))
1288
+ }
1289
+ }
1290
+ }
1291
+ "browser_click" => {
1292
+ let selector = if !input.selector.is_empty() {
1293
+ &input.selector
1294
+ } else if !input.path.is_empty() {
1295
+ &input.path
1296
+ } else {
1297
+ return Err(OrchestrationError::Agent(
1298
+ "browser_click requires 'selector'".into(),
1299
+ ));
1300
+ };
1301
+ let result = crate::browser::click(config, selector)
1302
+ .await
1303
+ .map_err(|e| OrchestrationError::Agent(e))?;
1304
+ Ok(result)
1305
+ }
1306
+ "browser_type" => {
1307
+ let selector = if !input.selector.is_empty() {
1308
+ &input.selector
1309
+ } else if !input.path.is_empty() {
1310
+ &input.path
1311
+ } else {
1312
+ return Err(OrchestrationError::Agent(
1313
+ "browser_type requires 'selector'".into(),
1314
+ ));
1315
+ };
1316
+ let text = if !input.text.is_empty() {
1317
+ &input.text
1318
+ } else if !input.query.is_empty() {
1319
+ &input.query
1320
+ } else {
1321
+ return Err(OrchestrationError::Agent(
1322
+ "browser_type requires 'text'".into(),
1323
+ ));
1324
+ };
1325
+ let result = crate::browser::type_text(config, selector, text)
1326
+ .await
1327
+ .map_err(|e| OrchestrationError::Agent(e))?;
1328
+ Ok(result)
1329
+ }
1330
+ "browser_read" => {
1331
+ let result = crate::browser::read_page_text(config)
1332
+ .await
1333
+ .map_err(|e| OrchestrationError::Agent(e))?;
1334
+ Ok(result)
1335
+ }
1079
1336
  "memory_recall" => {
1080
1337
  let query = required(&input.query, "query")?;
1081
1338
  let query_lower = query.to_ascii_lowercase();
@@ -1640,8 +1897,9 @@ fn action_fingerprint(decision: &AgentDecision) -> String {
1640
1897
  }
1641
1898
 
1642
1899
  fn initial_observation(task: &str, root: &Path, skills: &str) -> String {
1900
+ let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string();
1643
1901
  let mut observation = format!(
1644
- "Task: {task}\nWorkspace: {}\nLearned skills:\n{}\n",
1902
+ "Current Time: {now}\nTask: {task}\nWorkspace: {}\nLearned skills:\n{}\n",
1645
1903
  root.display(),
1646
1904
  if skills.trim().is_empty() {
1647
1905
  "(none)"
@@ -1991,6 +2249,7 @@ Example response:
1991
2249
  audio_data_uri: None,
1992
2250
  document_attachment: None,
1993
2251
  workspace_path: None,
2252
+ agent_id: None,
1994
2253
  };
1995
2254
 
1996
2255
  // Send the chat request to LLM
@@ -2049,6 +2308,7 @@ mod tests {
2049
2308
  audio_data_uri: None,
2050
2309
  document_attachment: None,
2051
2310
  workspace_path: None,
2311
+ agent_id: None,
2052
2312
  };
2053
2313
  assert_eq!(
2054
2314
  enrich_request(&store, &request).unwrap().system_instruction,
@@ -2154,4 +2414,49 @@ mod tests {
2154
2414
 
2155
2415
  assert_eq!(classification.mode.as_str(), "readOnly");
2156
2416
  }
2417
+
2418
+ #[test]
2419
+ fn test_resolve_agent_config() {
2420
+ let mut config = MintConfig::default();
2421
+ config.agents = vec![
2422
+ AgentConfig {
2423
+ id: "planner".into(),
2424
+ name: "Planner".into(),
2425
+ provider: "openai".into(),
2426
+ model: "gpt-4o".into(),
2427
+ api_key: Some("test-key".into()),
2428
+ system_instruction: "Planner instruction".into(),
2429
+ enabled: true,
2430
+ },
2431
+ AgentConfig {
2432
+ id: "coder".into(),
2433
+ name: "Coder".into(),
2434
+ provider: "gemini".into(),
2435
+ model: "gemini-2.5-flash".into(),
2436
+ api_key: None,
2437
+ system_instruction: "Coder instruction".into(),
2438
+ enabled: true,
2439
+ },
2440
+ ];
2441
+
2442
+ let (cfg, instr, name, model) = resolve_agent_config(&config, Some("planner"), &[]);
2443
+ assert_eq!(cfg.ai_provider, "openai");
2444
+ assert_eq!(cfg.openai_model, "gpt-4o");
2445
+ assert_eq!(cfg.openai_api_key, "test-key");
2446
+ assert_eq!(instr, "Planner instruction");
2447
+ assert_eq!(name, Some("Planner".to_string()));
2448
+ assert_eq!(model, Some("gpt-4o".to_string()));
2449
+
2450
+ let (cfg, instr, name, model) = resolve_agent_config(&config, None, &[]);
2451
+ assert_eq!(cfg.ai_provider, "openai");
2452
+ assert_eq!(instr, "Planner instruction");
2453
+ assert_eq!(name, Some("Planner".to_string()));
2454
+
2455
+ let trajectory = vec!["Step 1:\n- Action: create_plan\n- Observation: all planned".to_string()];
2456
+ let (cfg, instr, name, model) = resolve_agent_config(&config, None, &trajectory);
2457
+ assert_eq!(cfg.ai_provider, "gemini");
2458
+ assert_eq!(instr, "Coder instruction");
2459
+ assert_eq!(name, Some("Coder".to_string()));
2460
+ assert_eq!(model, Some("gemini-2.5-flash".to_string()));
2461
+ }
2157
2462
  }
@@ -53,10 +53,32 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
53
53
  Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
54
54
  }
55
55
 
56
- pub fn learned_skills_context() -> Result<String, SkillError> {
57
- let skills = MemoryStore::open_default()?.learned_skills(20)?;
58
- let value = skills
59
- .into_iter()
56
+ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, SkillError> {
57
+ let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
58
+
59
+ if let Some(home) = dirs::home_dir() {
60
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
61
+ if !global_skills_path.exists() {
62
+ let _ = std::fs::create_dir_all(&global_skills_path);
63
+ }
64
+ load_skills_from_dir(&global_skills_path, &mut skills);
65
+ }
66
+
67
+ if let Some(root) = workspace_root {
68
+ let workspace_skills_path1 = root.join(".agents").join("skills");
69
+ load_skills_from_dir(&workspace_skills_path1, &mut skills);
70
+
71
+ let workspace_skills_path2 = root.join("skills");
72
+ load_skills_from_dir(&workspace_skills_path2, &mut skills);
73
+ }
74
+
75
+ let mut unique_skills = std::collections::BTreeMap::new();
76
+ for skill in skills {
77
+ unique_skills.insert(skill.name.clone(), skill);
78
+ }
79
+
80
+ let value = unique_skills
81
+ .into_values()
60
82
  .map(|skill| format!("Skill: {}\n{}", skill.name, skill.content))
61
83
  .collect::<Vec<_>>()
62
84
  .join("\n\n");
@@ -69,3 +91,90 @@ pub fn learned_skills_context() -> Result<String, SkillError> {
69
91
  }
70
92
  Ok(format!("{}\n...<learned skills truncated>", &value[..end]))
71
93
  }
94
+
95
+ pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
96
+ if !dir.is_dir() {
97
+ return;
98
+ }
99
+ if let Ok(entries) = fs::read_dir(dir) {
100
+ for entry in entries.flatten() {
101
+ let path = entry.path();
102
+ if path.is_dir() {
103
+ for filename in &["SKILL.md", "SKILL.txt", "skill.md", "skill.txt"] {
104
+ let skill_file = path.join(filename);
105
+ if skill_file.is_file() {
106
+ if let Ok(content) = fs::read_to_string(&skill_file) {
107
+ let name = path
108
+ .file_name()
109
+ .and_then(|n| n.to_str())
110
+ .unwrap_or("skill")
111
+ .to_string();
112
+ list.push(LearnedSkill {
113
+ id: 0,
114
+ name,
115
+ source_path: skill_file.to_string_lossy().to_string(),
116
+ content,
117
+ created_at: String::new(),
118
+ });
119
+ }
120
+ break;
121
+ }
122
+ }
123
+ } else if path.is_file() {
124
+ if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
125
+ if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
126
+ if let Ok(content) = fs::read_to_string(&path) {
127
+ let name = path
128
+ .file_stem()
129
+ .and_then(|n| n.to_str())
130
+ .unwrap_or("skill")
131
+ .to_string();
132
+ list.push(LearnedSkill {
133
+ id: 0,
134
+ name,
135
+ source_path: path.to_string_lossy().to_string(),
136
+ content,
137
+ created_at: String::new(),
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ #[cfg(test)]
148
+ mod tests {
149
+ use super::*;
150
+
151
+ #[test]
152
+ fn test_fs_skills_loading() {
153
+ let test_dir = std::env::temp_dir().join("mint_skills_test_unique_dir");
154
+ let _ = std::fs::remove_dir_all(&test_dir);
155
+ let workspace_path = test_dir.join(".agents").join("skills");
156
+ std::fs::create_dir_all(&workspace_path).unwrap();
157
+
158
+ // Write a file skill
159
+ let file_skill_path = workspace_path.join("rust-style.md");
160
+ std::fs::write(&file_skill_path, "Use 4 spaces for Rust indent.").unwrap();
161
+
162
+ // Write a directory skill
163
+ let dir_skill_path = workspace_path.join("js-style");
164
+ std::fs::create_dir_all(&dir_skill_path).unwrap();
165
+ std::fs::write(dir_skill_path.join("SKILL.md"), "Use 2 spaces for JS.").unwrap();
166
+
167
+ let mut list = Vec::new();
168
+ load_skills_from_dir(&workspace_path, &mut list);
169
+
170
+ let _ = std::fs::remove_dir_all(&test_dir);
171
+
172
+ assert_eq!(list.len(), 2);
173
+
174
+ let rust_skill = list.iter().find(|s| s.name == "rust-style").unwrap();
175
+ assert_eq!(rust_skill.content, "Use 4 spaces for Rust indent.");
176
+
177
+ let js_skill = list.iter().find(|s| s.name == "js-style").unwrap();
178
+ assert_eq!(js_skill.content, "Use 2 spaces for JS.");
179
+ }
180
+ }
@@ -10,6 +10,8 @@ pub struct TtsUrl {
10
10
  }
11
11
 
12
12
  pub fn google_tts_urls(text: &str, language: &str) -> Vec<TtsUrl> {
13
+ let has_thai = text.chars().any(|c| ('\u{0e00}'..='\u{0e7f}').contains(&c));
14
+ let tl = if has_thai { "th" } else { language };
13
15
  let chunks = split_text(text, MAX_GOOGLE_TTS_CHARS);
14
16
  let total = chunks.len();
15
17
  chunks
@@ -19,7 +21,7 @@ pub fn google_tts_urls(text: &str, language: &str) -> Vec<TtsUrl> {
19
21
  url: format!(
20
22
  "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl={}&client=tw-ob&idx={index}&total={total}&textlen={}",
21
23
  encode_component(&chunk),
22
- encode_component(language),
24
+ encode_component(tl),
23
25
  chunk.chars().count()
24
26
  ),
25
27
  short_text: chunk,
@@ -51,28 +51,7 @@ pub fn load_workflows() -> Result<Vec<Value>, WorkflowError> {
51
51
  fn save_default_workflows(path: &PathBuf) -> Result<(), WorkflowError> {
52
52
  let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
53
53
  fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
54
- let workflows = json!([
55
- {
56
- "id": "wf-1",
57
- "name": "Check Mic on Zoom",
58
- "trigger": { "type": "process_running", "processName": "zoom" },
59
- "action": {
60
- "type": "system_info",
61
- "message": "Looks like you opened Zoom. Should I check your system resources?",
62
- "target": ""
63
- }
64
- },
65
- {
66
- "id": "wf-2",
67
- "name": "Coding Time",
68
- "trigger": { "type": "process_running", "processName": "code" },
69
- "action": {
70
- "type": "open_app",
71
- "message": "Coding time. Want me to open Spotify?",
72
- "target": "spotify"
73
- }
74
- }
75
- ]);
54
+ let workflows = json!([]);
76
55
  let raw = serde_json::to_string_pretty(&workflows).map_err(WorkflowError::Serialize)?;
77
56
  fs::write(path, format!("{raw}\n")).map_err(|source| WorkflowError::Write {
78
57
  path: path.clone(),