@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
@@ -213,16 +213,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
213
213
  new_title: String,
214
214
  }
215
215
 
216
- if let Ok(req) = serde_json::from_str::<RenameRequest>(body) {
217
- if let Ok(memory) = MemoryStore::open_default() {
218
- let updated = memory
219
- .rename_chat_session(&req.chat_id, &req.new_title)
220
- .unwrap_or(0);
221
- let response =
222
- serde_json::json!({ "status": "ok", "updated": updated });
223
- send_json_response(socket, "200 OK", &response.to_string()).await;
224
- return;
225
- }
216
+ if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
217
+ && let Ok(memory) = MemoryStore::open_default()
218
+ {
219
+ let updated = memory
220
+ .rename_chat_session(&req.chat_id, &req.new_title)
221
+ .unwrap_or(0);
222
+ let response = serde_json::json!({ "status": "ok", "updated": updated });
223
+ send_json_response(socket, "200 OK", &response.to_string()).await;
224
+ return;
226
225
  }
227
226
  send_json_response(
228
227
  socket,
@@ -252,12 +251,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
252
251
  key: String,
253
252
  value: String,
254
253
  }
255
- if let Ok(req) = serde_json::from_str::<ProfileRequest>(body) {
256
- if let Ok(memory) = MemoryStore::open_default() {
257
- let _ = memory.set_profile(&req.key, &req.value);
258
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
259
- return;
260
- }
254
+ if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
255
+ && let Ok(memory) = MemoryStore::open_default()
256
+ {
257
+ let _ = memory.set_profile(&req.key, &req.value);
258
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
259
+ return;
261
260
  }
262
261
  send_json_response(
263
262
  socket,
@@ -341,25 +340,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
341
340
  activity: Vec<AgentProgress>,
342
341
  }
343
342
 
344
- if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body) {
345
- if let Ok(memory) = MemoryStore::open_default() {
346
- match serde_json::to_string(&payload.activity) {
347
- Ok(activity_json) => {
348
- if memory
349
- .set_interaction_agent_activity_json(
350
- payload.interaction_id,
351
- &activity_json,
352
- )
353
- .is_ok()
354
- {
355
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}")
356
- .await;
357
- return;
358
- }
359
- }
360
- Err(_) => {}
361
- }
362
- }
343
+ if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
344
+ && let Ok(memory) = MemoryStore::open_default()
345
+ && let Ok(activity_json) = serde_json::to_string(&payload.activity)
346
+ && memory
347
+ .set_interaction_agent_activity_json(
348
+ payload.interaction_id,
349
+ &activity_json,
350
+ )
351
+ .is_ok()
352
+ {
353
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
354
+ return;
363
355
  }
364
356
  send_json_response(
365
357
  socket,
@@ -406,11 +398,11 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
406
398
  }
407
399
  }
408
400
  ("POST", "/api/config") => {
409
- if let Ok(new_config) = serde_json::from_str::<MintConfig>(body) {
410
- if save_config(&new_config).is_ok() {
411
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
412
- return;
413
- }
401
+ if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
402
+ && save_config(&new_config).is_ok()
403
+ {
404
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
405
+ return;
414
406
  }
415
407
  send_json_response(
416
408
  socket,
@@ -476,6 +468,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
476
468
  image_data_uri: Option<String>,
477
469
  audio_data_uri: Option<String>,
478
470
  document_attachment: Option<crate::chat::DocumentAttachment>,
471
+ agent_id: Option<String>,
479
472
  }
480
473
 
481
474
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -488,6 +481,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
488
481
  audio_data_uri: req.audio_data_uri,
489
482
  document_attachment: req.document_attachment,
490
483
  workspace_path: None,
484
+ agent_id: req.agent_id,
491
485
  };
492
486
  let mut chat_req =
493
487
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -550,6 +544,24 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
550
544
  )
551
545
  .await;
552
546
  }
547
+ ("POST", "/api/cancel-chat") => {
548
+ #[derive(Deserialize)]
549
+ #[serde(rename_all = "camelCase")]
550
+ struct CancelRequest {
551
+ chat_id: String,
552
+ }
553
+ if let Ok(req) = serde_json::from_str::<CancelRequest>(body) {
554
+ crate::cancel_agent(&req.chat_id);
555
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
556
+ } else {
557
+ send_json_response(
558
+ socket,
559
+ "400 Bad Request",
560
+ "{\"status\":\"invalid cancel body\"}",
561
+ )
562
+ .await;
563
+ }
564
+ }
553
565
  ("POST", "/api/chat-stream") => {
554
566
  #[derive(Deserialize)]
555
567
  #[serde(rename_all = "camelCase")]
@@ -560,6 +572,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
560
572
  image_data_uri: Option<String>,
561
573
  audio_data_uri: Option<String>,
562
574
  document_attachment: Option<crate::chat::DocumentAttachment>,
575
+ agent_id: Option<String>,
563
576
  }
564
577
 
565
578
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -572,6 +585,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
572
585
  audio_data_uri: req.audio_data_uri,
573
586
  document_attachment: req.document_attachment,
574
587
  workspace_path: None,
588
+ agent_id: req.agent_id,
575
589
  };
576
590
  let mut chat_req =
577
591
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -631,7 +645,8 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
631
645
  let config_clone = config.clone();
632
646
  let chat_req_clone = chat_req.clone();
633
647
  let tx_done = tx.clone();
634
- tokio::spawn(async move {
648
+ let chat_id_str = chat_req.chat_id.clone().unwrap_or_default();
649
+ let join_handle = tokio::spawn(async move {
635
650
  let result = orchestrate_chat_stream_with_fallback(
636
651
  &config_clone,
637
652
  &chat_req_clone,
@@ -670,11 +685,29 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
670
685
  "text": format!("Error orchestrating chat: {e}")
671
686
  }
672
687
  });
673
- let _ = tx_done
674
- .send(format!("{}\n", err_json.to_string()));
688
+ let _ = tx_done.send(format!("{}\n", err_json));
675
689
  }
676
690
  }
677
691
  });
692
+
693
+ let abort_handle = join_handle.abort_handle();
694
+ if !chat_id_str.is_empty() {
695
+ crate::ACTIVE_AGENTS
696
+ .lock()
697
+ .unwrap()
698
+ .insert(chat_id_str.clone(), abort_handle);
699
+ }
700
+
701
+ let chat_id_str_cleanup = chat_id_str.clone();
702
+ tokio::spawn(async move {
703
+ let _ = join_handle.await;
704
+ if !chat_id_str_cleanup.is_empty() {
705
+ crate::ACTIVE_AGENTS
706
+ .lock()
707
+ .unwrap()
708
+ .remove(&chat_id_str_cleanup);
709
+ }
710
+ });
678
711
  } else {
679
712
  let root = std::env::current_dir().unwrap_or_default();
680
713
  let fast_mode = config
@@ -686,16 +719,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
686
719
  let tx_done = tx.clone();
687
720
  let config_clone = config.clone();
688
721
  let chat_id = chat_req.chat_id.clone();
722
+ let chat_id_str = chat_id.clone().unwrap_or_default();
689
723
  let message = chat_req.message.clone();
690
724
  let image_data_uri = chat_req.image_data_uri.clone();
725
+ let agent_id = chat_req.agent_id.clone();
691
726
 
692
- tokio::spawn(async move {
727
+ let join_handle = tokio::spawn(async move {
693
728
  let result = orchestrate_agent_loop(
694
729
  &config_clone,
695
730
  &message,
696
731
  &root,
697
732
  image_data_uri,
698
733
  chat_id.as_deref(),
734
+ agent_id.as_deref(),
699
735
  fast_mode,
700
736
  |_| Ok(ApprovalOutcome::Denied),
701
737
  progress_cb,
@@ -729,11 +765,29 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
729
765
  "text": format!("Error orchestrating agent: {e}")
730
766
  }
731
767
  });
732
- let _ = tx_done
733
- .send(format!("{}\n", err_json.to_string()));
768
+ let _ = tx_done.send(format!("{}\n", err_json));
734
769
  }
735
770
  }
736
771
  });
772
+
773
+ let abort_handle = join_handle.abort_handle();
774
+ if !chat_id_str.is_empty() {
775
+ crate::ACTIVE_AGENTS
776
+ .lock()
777
+ .unwrap()
778
+ .insert(chat_id_str.clone(), abort_handle);
779
+ }
780
+
781
+ let chat_id_str_cleanup = chat_id_str.clone();
782
+ tokio::spawn(async move {
783
+ let _ = join_handle.await;
784
+ if !chat_id_str_cleanup.is_empty() {
785
+ crate::ACTIVE_AGENTS
786
+ .lock()
787
+ .unwrap()
788
+ .remove(&chat_id_str_cleanup);
789
+ }
790
+ });
737
791
  }
738
792
  }
739
793
 
@@ -894,6 +948,7 @@ async fn run_web_agent_loop(
894
948
  &root,
895
949
  request.image_data_uri.clone(),
896
950
  request.chat_id.as_deref(),
951
+ request.agent_id.as_deref(),
897
952
  fast_mode,
898
953
  |_| Ok(ApprovalOutcome::Denied),
899
954
  |_| {},
@@ -1082,12 +1137,13 @@ fn percent_decode(raw: &str) -> String {
1082
1137
  let mut output = Vec::with_capacity(bytes.len());
1083
1138
  let mut index = 0;
1084
1139
  while index < bytes.len() {
1085
- if bytes[index] == b'%' && index + 2 < bytes.len() {
1086
- if let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16) {
1087
- output.push(hex);
1088
- index += 3;
1089
- continue;
1090
- }
1140
+ if bytes[index] == b'%'
1141
+ && index + 2 < bytes.len()
1142
+ && let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16)
1143
+ {
1144
+ output.push(hex);
1145
+ index += 3;
1146
+ continue;
1091
1147
  }
1092
1148
  output.push(if bytes[index] == b'+' {
1093
1149
  b' '
@@ -243,7 +243,7 @@ pub async fn spawn_automation_browser(config: &MintConfig) -> Result<(), String>
243
243
  .unwrap_or("chromium");
244
244
 
245
245
  let profile_dir = dirs::config_dir()
246
- .unwrap_or_else(|| std::env::temp_dir())
246
+ .unwrap_or_else(std::env::temp_dir)
247
247
  .join("mint")
248
248
  .join("automation-profile");
249
249
  let profile_arg = format!("--user-data-dir={}", profile_dir.to_string_lossy());
@@ -340,6 +340,61 @@ async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Va
340
340
  let (mut socket, _) = connect_async(socket_url)
341
341
  .await
342
342
  .map_err(|error| format!("unable to connect to Chrome DevTools websocket: {error}"))?;
343
+ let is_aura_script = method == "Runtime.evaluate"
344
+ && params["expression"]
345
+ .as_str()
346
+ .map(|s| s.contains("mint-browser-aura"))
347
+ .unwrap_or(false);
348
+
349
+ if !is_aura_script {
350
+ let aura_script = r#"
351
+ (() => {
352
+ if (document.getElementById('mint-browser-aura')) return;
353
+ const aura = document.createElement('div');
354
+ aura.id = 'mint-browser-aura';
355
+
356
+ const style = document.createElement('style');
357
+ style.id = 'mint-browser-aura-style';
358
+ style.textContent = `
359
+ @keyframes mint-pulse {
360
+ 0% { box-shadow: inset 0 0 15px rgba(16, 185, 129, 0.4); border-color: rgba(16, 185, 129, 0.6); }
361
+ 100% { box-shadow: inset 0 0 30px rgba(16, 185, 129, 0.8); border-color: rgba(16, 185, 129, 1); }
362
+ }
363
+ `;
364
+ document.head.appendChild(style);
365
+
366
+ aura.style.cssText = `
367
+ position: fixed;
368
+ top: 0;
369
+ left: 0;
370
+ width: 100vw;
371
+ height: 100vh;
372
+ border: 5px solid rgba(16, 185, 129, 0.6);
373
+ pointer-events: none;
374
+ z-index: 2147483647;
375
+ box-sizing: border-box;
376
+ animation: mint-pulse 1.5s infinite alternate;
377
+ `;
378
+ document.body.appendChild(aura);
379
+ })()
380
+ "#;
381
+
382
+ let _ = socket
383
+ .send(Message::Text(
384
+ json!({
385
+ "id": 999,
386
+ "method": "Runtime.evaluate",
387
+ "params": {
388
+ "expression": aura_script,
389
+ "returnByValue": true
390
+ }
391
+ })
392
+ .to_string()
393
+ .into(),
394
+ ))
395
+ .await;
396
+ }
397
+
343
398
  socket
344
399
  .send(Message::Text(
345
400
  json!({ "id": 1, "method": method, "params": params })
@@ -194,6 +194,7 @@ async fn answer(text: &str, system_instruction: &str) -> String {
194
194
  audio_data_uri: None,
195
195
  document_attachment: None,
196
196
  workspace_path: None,
197
+ agent_id: None,
197
198
  },
198
199
  )
199
200
  .await
@@ -19,13 +19,13 @@ pub async fn send_chat_with_fallback(
19
19
  Err(_) => {}
20
20
  }
21
21
  for provider in config.available_providers() {
22
- if provider == config.ai_provider.as_str() {
22
+ if provider.as_str() == config.ai_provider.as_str() {
23
23
  continue;
24
24
  }
25
- let alt = config_for_provider(config, provider);
25
+ let alt = config_for_provider(config, &provider);
26
26
  if let Ok(mut r) = send_chat(&alt, request).await {
27
27
  r.fallback_provider = Some(config.ai_provider.clone());
28
- return Ok((r, Some(provider.to_owned())));
28
+ return Ok((r, Some(provider)));
29
29
  }
30
30
  }
31
31
  // all fallbacks failed — retry primary to surface original error
@@ -50,13 +50,13 @@ where
50
50
  }
51
51
  }
52
52
  for provider in config.available_providers() {
53
- if provider == config.ai_provider.as_str() {
53
+ if provider.as_str() == config.ai_provider.as_str() {
54
54
  continue;
55
55
  }
56
- let alt = config_for_provider(config, provider);
56
+ let alt = config_for_provider(config, &provider);
57
57
  if let Ok(mut r) = stream_chat(&alt, request, &mut on_chunk).await {
58
58
  r.fallback_provider = Some(config.ai_provider.clone());
59
- return Ok((r, Some(provider.to_owned())));
59
+ return Ok((r, Some(provider)));
60
60
  }
61
61
  }
62
62
  stream_chat(config, request, &mut on_chunk)
@@ -99,6 +99,8 @@ pub struct ChatRequest {
99
99
  pub document_attachment: Option<DocumentAttachment>,
100
100
  #[serde(default)]
101
101
  pub workspace_path: Option<String>,
102
+ #[serde(default)]
103
+ pub agent_id: Option<String>,
102
104
  }
103
105
 
104
106
  #[derive(Debug, Clone, Deserialize)]
@@ -228,6 +230,7 @@ pub async fn send_chat(
228
230
  "ollama" => call_ollama(&client, config, request).await?,
229
231
  "anthropic" => call_anthropic(&client, config, request).await?,
230
232
  "huggingface" => call_huggingface(&client, config, request).await?,
233
+ p if p.starts_with("custom:") => call_custom_provider(&client, config, request).await?,
231
234
  other => return Err(ChatError::UnsupportedProvider(other.into())),
232
235
  };
233
236
  Ok(ChatResponse {
@@ -257,6 +260,9 @@ where
257
260
  "ollama" => stream_ollama(&client, config, request, &mut on_chunk).await?,
258
261
  "anthropic" => stream_anthropic(&client, config, request, &mut on_chunk).await?,
259
262
  "huggingface" => stream_huggingface(&client, config, request, &mut on_chunk).await?,
263
+ p if p.starts_with("custom:") => {
264
+ stream_custom_provider(&client, config, request, &mut on_chunk).await?
265
+ }
260
266
  other => return Err(ChatError::UnsupportedProvider(other.into())),
261
267
  };
262
268
  Ok(ChatResponse {
@@ -706,6 +712,109 @@ fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
706
712
  }
707
713
  }
708
714
 
715
+ /// Resolve which model to use for the current custom provider.
716
+ ///
717
+ /// Checks `config.extra["customModelSelections"]` first (set by the UI when the
718
+ /// user picks a specific model from the multi-model dropdown), then falls back to
719
+ /// the first model defined in the `CustomProvider`.
720
+ fn resolve_custom_model(config: &crate::MintConfig) -> String {
721
+ let provider_id = config
722
+ .ai_provider
723
+ .strip_prefix("custom:")
724
+ .unwrap_or(&config.ai_provider);
725
+
726
+ // Check if a per-provider model selection was persisted in extra config.
727
+ if let Some(selections) = config
728
+ .extra
729
+ .get("customModelSelections")
730
+ .and_then(|v| v.as_object())
731
+ && let Some(model) = selections.get(provider_id).and_then(|v| v.as_str())
732
+ && !model.is_empty()
733
+ {
734
+ return model.to_owned();
735
+ }
736
+
737
+ // Fall back to the first model in the provider definition.
738
+ config
739
+ .resolve_custom_provider(&config.ai_provider)
740
+ .and_then(|cp| cp.models.first())
741
+ .map(|m| m.model_id.clone())
742
+ .unwrap_or_default()
743
+ }
744
+
745
+ async fn call_custom_provider(
746
+ client: &Client,
747
+ config: &crate::MintConfig,
748
+ request: &ChatRequest,
749
+ ) -> Result<(String, String), ChatError> {
750
+ let provider = config
751
+ .resolve_custom_provider(&config.ai_provider)
752
+ .ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
753
+
754
+ let base_url = provider.base_url.trim_end_matches('/');
755
+ let model = resolve_custom_model(config);
756
+ let payload = openai_chat_payload(&model, request, false);
757
+
758
+ let mut builder = client
759
+ .post(format!("{base_url}/chat/completions"))
760
+ .json(&payload);
761
+
762
+ if !provider.api_key.is_empty() {
763
+ builder = builder.bearer_auth(&provider.api_key);
764
+ }
765
+ for h in &provider.headers {
766
+ if !h.name.is_empty() {
767
+ builder = builder.header(h.name.as_str(), h.value.as_str());
768
+ }
769
+ }
770
+
771
+ let response: serde_json::Value = builder.send().await?.error_for_status()?.json().await?;
772
+
773
+ Ok((
774
+ model,
775
+ response["choices"][0]["message"]["content"]
776
+ .as_str()
777
+ .ok_or(ChatError::MissingResponseText)?
778
+ .into(),
779
+ ))
780
+ }
781
+
782
+ async fn stream_custom_provider<F>(
783
+ client: &Client,
784
+ config: &crate::MintConfig,
785
+ request: &ChatRequest,
786
+ on_chunk: &mut F,
787
+ ) -> Result<(String, String), ChatError>
788
+ where
789
+ F: FnMut(String),
790
+ {
791
+ let provider = config
792
+ .resolve_custom_provider(&config.ai_provider)
793
+ .ok_or_else(|| ChatError::UnsupportedProvider(config.ai_provider.clone()))?;
794
+
795
+ let base_url = provider.base_url.trim_end_matches('/');
796
+ let model = resolve_custom_model(config);
797
+ let payload = openai_chat_payload(&model, request, true);
798
+
799
+ let mut builder = client
800
+ .post(format!("{base_url}/chat/completions"))
801
+ .json(&payload);
802
+
803
+ if !provider.api_key.is_empty() {
804
+ builder = builder.bearer_auth(&provider.api_key);
805
+ }
806
+ for h in &provider.headers {
807
+ if !h.name.is_empty() {
808
+ builder = builder.header(h.name.as_str(), h.value.as_str());
809
+ }
810
+ }
811
+
812
+ let response = builder.send().await?.error_for_status()?;
813
+ collect_stream(response, StreamFormat::OpenAi, on_chunk)
814
+ .await
815
+ .map(|text| (model, text))
816
+ }
817
+
709
818
  fn provider_key(configured: &str, environment_variable: &str) -> String {
710
819
  if configured.trim().is_empty() {
711
820
  std::env::var(environment_variable).unwrap_or_default()
@@ -718,16 +827,24 @@ fn require_supported_attachments(provider: &str, request: &ChatRequest) -> Resul
718
827
  let has_image = request
719
828
  .image_data_uri
720
829
  .as_ref()
721
- .map_or(false, |s| !s.trim().is_empty());
830
+ .is_some_and(|s| !s.trim().is_empty());
722
831
  let has_audio = request
723
832
  .audio_data_uri
724
833
  .as_ref()
725
- .map_or(false, |s| !s.trim().is_empty());
834
+ .is_some_and(|s| !s.trim().is_empty());
726
835
 
727
836
  match provider {
728
837
  "gemini" => Ok(()),
729
838
  "ollama" if has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
730
839
  "ollama" => Ok(()),
840
+ // Custom providers are text-only for now; reject any multimodal content.
841
+ p if p.starts_with("custom:") => {
842
+ if has_image || has_audio {
843
+ Err(ChatError::UnsupportedAttachments(provider.into()))
844
+ } else {
845
+ Ok(())
846
+ }
847
+ }
731
848
  _ if has_image || has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
732
849
  _ => Ok(()),
733
850
  }
@@ -965,6 +1082,7 @@ mod tests {
965
1082
  audio_data_uri: None,
966
1083
  document_attachment: None,
967
1084
  workspace_path: None,
1085
+ agent_id: None,
968
1086
  })
969
1087
  .unwrap();
970
1088
  assert_eq!(parts.len(), 3);
@@ -536,18 +536,17 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
536
536
  .await
537
537
  .map_err(|e| e.to_string())?;
538
538
  let mut file_tree = String::from("Unavailable");
539
- if contents_resp.status().is_success() {
540
- if let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await {
541
- if let Some(arr) = contents_info.as_array() {
542
- let mut files = Vec::new();
543
- for item in arr {
544
- let name = item["name"].as_str().unwrap_or("");
545
- let r#type = item["type"].as_str().unwrap_or("");
546
- files.push(format!("- {} ({})", name, r#type));
547
- }
548
- file_tree = files.join("\n");
549
- }
539
+ if contents_resp.status().is_success()
540
+ && let Ok(contents_info) = contents_resp.json::<serde_json::Value>().await
541
+ && let Some(arr) = contents_info.as_array()
542
+ {
543
+ let mut files = Vec::new();
544
+ for item in arr {
545
+ let name = item["name"].as_str().unwrap_or("");
546
+ let r#type = item["type"].as_str().unwrap_or("");
547
+ files.push(format!("- {} ({})", name, r#type));
550
548
  }
549
+ file_tree = files.join("\n");
551
550
  }
552
551
 
553
552
  // 3. Fetch README.md
@@ -558,15 +557,14 @@ pub async fn fetch_github_repo_summary(owner: &str, repo: &str) -> Result<String
558
557
  .await
559
558
  .map_err(|e| e.to_string())?;
560
559
  let mut readme_text = String::from("No README available.");
561
- if readme_resp.status().is_success() {
562
- if let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await {
563
- if let Some(content_b64) = readme_info["content"].as_str() {
564
- let cleaned_b64 = content_b64.replace('\n', "").replace('\r', "");
565
- use base64::{Engine as _, engine::general_purpose::STANDARD};
566
- if let Ok(decoded_bytes) = STANDARD.decode(cleaned_b64) {
567
- readme_text = String::from_utf8_lossy(&decoded_bytes).to_string();
568
- }
569
- }
560
+ if readme_resp.status().is_success()
561
+ && let Ok(readme_info) = readme_resp.json::<serde_json::Value>().await
562
+ && let Some(content_b64) = readme_info["content"].as_str()
563
+ {
564
+ let cleaned_b64 = content_b64.replace(['\n', '\r'], "");
565
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
566
+ if let Ok(decoded_bytes) = STANDARD.decode(cleaned_b64) {
567
+ readme_text = String::from_utf8_lossy(&decoded_bytes).to_string();
570
568
  }
571
569
  }
572
570