@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
@@ -61,6 +61,7 @@ where
61
61
  audio_data_uri: None,
62
62
  document_attachment: None,
63
63
  workspace_path: None,
64
+ agent_id: None,
64
65
  },
65
66
  )
66
67
  .await?;
@@ -147,6 +147,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
147
147
  });
148
148
  send_json_response(socket, "200 OK", &status_json.to_string()).await;
149
149
  }
150
+ ("GET", "/api/detect-tools") => {
151
+ let tools_json = serde_json::json!({
152
+ "docker": crate::config::which("docker"),
153
+ "git": crate::config::which("git"),
154
+ "gh": crate::config::which("gh"),
155
+ "node": crate::config::which("node")
156
+ });
157
+ send_json_response(socket, "200 OK", &tools_json.to_string()).await;
158
+ }
150
159
  ("GET", "/api/system-info") => {
151
160
  send_json_response(socket, "200 OK", &system_info().to_string()).await;
152
161
  }
@@ -467,6 +476,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
467
476
  image_data_uri: Option<String>,
468
477
  audio_data_uri: Option<String>,
469
478
  document_attachment: Option<crate::chat::DocumentAttachment>,
479
+ agent_id: Option<String>,
470
480
  }
471
481
 
472
482
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -479,6 +489,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
479
489
  audio_data_uri: req.audio_data_uri,
480
490
  document_attachment: req.document_attachment,
481
491
  workspace_path: None,
492
+ agent_id: req.agent_id,
482
493
  };
483
494
  let mut chat_req =
484
495
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -541,6 +552,24 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
541
552
  )
542
553
  .await;
543
554
  }
555
+ ("POST", "/api/cancel-chat") => {
556
+ #[derive(Deserialize)]
557
+ #[serde(rename_all = "camelCase")]
558
+ struct CancelRequest {
559
+ chat_id: String,
560
+ }
561
+ if let Ok(req) = serde_json::from_str::<CancelRequest>(body) {
562
+ crate::cancel_agent(&req.chat_id);
563
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
564
+ } else {
565
+ send_json_response(
566
+ socket,
567
+ "400 Bad Request",
568
+ "{\"status\":\"invalid cancel body\"}",
569
+ )
570
+ .await;
571
+ }
572
+ }
544
573
  ("POST", "/api/chat-stream") => {
545
574
  #[derive(Deserialize)]
546
575
  #[serde(rename_all = "camelCase")]
@@ -551,6 +580,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
551
580
  image_data_uri: Option<String>,
552
581
  audio_data_uri: Option<String>,
553
582
  document_attachment: Option<crate::chat::DocumentAttachment>,
583
+ agent_id: Option<String>,
554
584
  }
555
585
 
556
586
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
@@ -563,6 +593,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
563
593
  audio_data_uri: req.audio_data_uri,
564
594
  document_attachment: req.document_attachment,
565
595
  workspace_path: None,
596
+ agent_id: req.agent_id,
566
597
  };
567
598
  let mut chat_req =
568
599
  chat_req.with_document_context(&config).unwrap_or(chat_req);
@@ -622,7 +653,8 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
622
653
  let config_clone = config.clone();
623
654
  let chat_req_clone = chat_req.clone();
624
655
  let tx_done = tx.clone();
625
- tokio::spawn(async move {
656
+ let chat_id_str = chat_req.chat_id.clone().unwrap_or_default();
657
+ let join_handle = tokio::spawn(async move {
626
658
  let result = orchestrate_chat_stream_with_fallback(
627
659
  &config_clone,
628
660
  &chat_req_clone,
@@ -666,6 +698,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
666
698
  }
667
699
  }
668
700
  });
701
+
702
+ let abort_handle = join_handle.abort_handle();
703
+ if !chat_id_str.is_empty() {
704
+ crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
705
+ }
706
+
707
+ let chat_id_str_cleanup = chat_id_str.clone();
708
+ tokio::spawn(async move {
709
+ let _ = join_handle.await;
710
+ if !chat_id_str_cleanup.is_empty() {
711
+ crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
712
+ }
713
+ });
669
714
  } else {
670
715
  let root = std::env::current_dir().unwrap_or_default();
671
716
  let fast_mode = config
@@ -677,16 +722,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
677
722
  let tx_done = tx.clone();
678
723
  let config_clone = config.clone();
679
724
  let chat_id = chat_req.chat_id.clone();
725
+ let chat_id_str = chat_id.clone().unwrap_or_default();
680
726
  let message = chat_req.message.clone();
681
727
  let image_data_uri = chat_req.image_data_uri.clone();
728
+ let agent_id = chat_req.agent_id.clone();
682
729
 
683
- tokio::spawn(async move {
730
+ let join_handle = tokio::spawn(async move {
684
731
  let result = orchestrate_agent_loop(
685
732
  &config_clone,
686
733
  &message,
687
734
  &root,
688
735
  image_data_uri,
689
736
  chat_id.as_deref(),
737
+ agent_id.as_deref(),
690
738
  fast_mode,
691
739
  |_| Ok(ApprovalOutcome::Denied),
692
740
  progress_cb,
@@ -725,6 +773,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
725
773
  }
726
774
  }
727
775
  });
776
+
777
+ let abort_handle = join_handle.abort_handle();
778
+ if !chat_id_str.is_empty() {
779
+ crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
780
+ }
781
+
782
+ let chat_id_str_cleanup = chat_id_str.clone();
783
+ tokio::spawn(async move {
784
+ let _ = join_handle.await;
785
+ if !chat_id_str_cleanup.is_empty() {
786
+ crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
787
+ }
788
+ });
728
789
  }
729
790
  }
730
791
 
@@ -885,6 +946,7 @@ async fn run_web_agent_loop(
885
946
  &root,
886
947
  request.image_data_uri.clone(),
887
948
  request.chat_id.as_deref(),
949
+ request.agent_id.as_deref(),
888
950
  fast_mode,
889
951
  |_| Ok(ApprovalOutcome::Denied),
890
952
  |_| {},
@@ -0,0 +1,456 @@
1
+ use crate::MintConfig;
2
+ use futures_util::{SinkExt, StreamExt};
3
+ use serde::Serialize;
4
+ use serde_json::{Value, json};
5
+ use tokio_tungstenite::{connect_async, tungstenite::Message};
6
+
7
+ fn log_action(action: &str, details: &str) {
8
+ if let Some(config_dir) = dirs::config_dir() {
9
+ let log_dir = config_dir.join("mint");
10
+ let log_file = log_dir.join("browser-automation.log");
11
+ let _ = std::fs::create_dir_all(&log_dir);
12
+ if let Ok(mut file) = std::fs::OpenOptions::new()
13
+ .create(true)
14
+ .append(true)
15
+ .open(&log_file)
16
+ {
17
+ use std::io::Write;
18
+ let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
19
+ let _ = writeln!(file, "[{}] [{}] {}", now, action, details);
20
+ }
21
+ }
22
+ }
23
+
24
+ #[derive(Debug, Clone, Serialize)]
25
+ #[serde(rename_all = "camelCase")]
26
+ pub struct BrowserTab {
27
+ pub id: String,
28
+ pub title: String,
29
+ pub url: String,
30
+ }
31
+
32
+ pub async fn list_tabs(config: &MintConfig) -> Result<Vec<BrowserTab>, String> {
33
+ if !is_browser_running(config).await {
34
+ return Err("Browser automation is not running. Please run 'mint auto' first.".to_string());
35
+ }
36
+ Ok(fetch_pages(config)
37
+ .await?
38
+ .into_iter()
39
+ .filter_map(|page| {
40
+ Some(BrowserTab {
41
+ id: page["id"].as_str()?.to_owned(),
42
+ title: page["title"].as_str().unwrap_or_default().to_owned(),
43
+ url: page["url"].as_str().unwrap_or_default().to_owned(),
44
+ })
45
+ })
46
+ .collect())
47
+ }
48
+
49
+ pub async fn navigate(config: &MintConfig, url: &str) -> Result<String, String> {
50
+ if !(url.starts_with("https://") || url.starts_with("http://")) {
51
+ log_action(
52
+ "NAVIGATE_ERROR",
53
+ "Browser navigation only supports http and https URLs",
54
+ );
55
+ return Err("browser navigation only supports http and https URLs".into());
56
+ }
57
+ log_action("NAVIGATE", &format!("Navigating to {url}"));
58
+ ensure_page_open(config).await?;
59
+ match cdp_call(config, "Page.navigate", json!({ "url": url })).await {
60
+ Ok(response) => {
61
+ if response["result"]["frameId"].as_str().is_some() {
62
+ log_action(
63
+ "NAVIGATE_SUCCESS",
64
+ &format!("Successfully navigated to {url}"),
65
+ );
66
+ Ok(format!("navigating to {url}"))
67
+ } else {
68
+ let err = response_error(&response);
69
+ log_action("NAVIGATE_ERROR", &format!("Failed: {err}"));
70
+ Err(err)
71
+ }
72
+ }
73
+ Err(e) => {
74
+ log_action("NAVIGATE_ERROR", &format!("Websocket error: {e}"));
75
+ Err(e)
76
+ }
77
+ }
78
+ }
79
+
80
+ pub async fn read_page_text(config: &MintConfig) -> Result<String, String> {
81
+ log_action("READ", "Reading page text content");
82
+ ensure_page_open(config).await?;
83
+ match cdp_call(
84
+ config,
85
+ "Runtime.evaluate",
86
+ json!({
87
+ "expression": "document.body ? document.body.innerText.substring(0, 12000) : ''",
88
+ "returnByValue": true
89
+ }),
90
+ )
91
+ .await
92
+ {
93
+ Ok(response) => {
94
+ if let Some(val) = response["result"]["result"]["value"].as_str() {
95
+ log_action(
96
+ "READ_SUCCESS",
97
+ &format!("Successfully read {} characters", val.len()),
98
+ );
99
+ Ok(val.to_owned())
100
+ } else {
101
+ let err = response_error(&response);
102
+ log_action("READ_ERROR", &format!("Failed: {err}"));
103
+ Err(err)
104
+ }
105
+ }
106
+ Err(e) => {
107
+ log_action("READ_ERROR", &format!("Websocket error: {e}"));
108
+ Err(e)
109
+ }
110
+ }
111
+ }
112
+
113
+ pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String> {
114
+ let selector = selector.trim();
115
+ if selector.is_empty() || selector.len() > 500 {
116
+ log_action(
117
+ "CLICK_ERROR",
118
+ "Browser selector must contain between 1 and 500 characters",
119
+ );
120
+ return Err("browser selector must contain between 1 and 500 characters".into());
121
+ }
122
+ log_action("CLICK", &format!("Clicking element '{selector}'"));
123
+ ensure_page_open(config).await?;
124
+ let selector_json = serde_json::to_string(selector).map_err(|error| error.to_string())?;
125
+ match cdp_call(
126
+ config,
127
+ "Runtime.evaluate",
128
+ json!({
129
+ "expression": format!(
130
+ "(() => {{ const element = document.querySelector({selector_json}); if (!element) return 'not-found'; element.click(); return 'clicked'; }})()"
131
+ ),
132
+ "returnByValue": true
133
+ }),
134
+ )
135
+ .await {
136
+ Ok(response) => {
137
+ match response["result"]["result"]["value"].as_str() {
138
+ Some("clicked") => {
139
+ log_action("CLICK_SUCCESS", &format!("Successfully clicked element '{selector}'"));
140
+ Ok("clicked".into())
141
+ }
142
+ Some("not-found") => {
143
+ let err = format!("browser selector not found: {selector}");
144
+ log_action("CLICK_ERROR", &err);
145
+ Err(err)
146
+ }
147
+ _ => {
148
+ let err = response_error(&response);
149
+ log_action("CLICK_ERROR", &err);
150
+ Err(err)
151
+ }
152
+ }
153
+ }
154
+ Err(e) => {
155
+ log_action("CLICK_ERROR", &format!("Websocket error: {e}"));
156
+ Err(e)
157
+ }
158
+ }
159
+ }
160
+
161
+ pub async fn type_text(config: &MintConfig, selector: &str, text: &str) -> Result<String, String> {
162
+ let selector = selector.trim();
163
+ if selector.is_empty() || selector.len() > 500 {
164
+ log_action(
165
+ "TYPE_ERROR",
166
+ "Browser selector must contain between 1 and 500 characters",
167
+ );
168
+ return Err("browser selector must contain between 1 and 500 characters".into());
169
+ }
170
+ log_action("TYPE", &format!("Typing text into element '{selector}'"));
171
+ ensure_page_open(config).await?;
172
+ let selector_json = serde_json::to_string(selector).map_err(|error| error.to_string())?;
173
+ let text_json = serde_json::to_string(text).map_err(|error| error.to_string())?;
174
+ match cdp_call(
175
+ config,
176
+ "Runtime.evaluate",
177
+ json!({
178
+ "expression": format!(
179
+ "(() => {{ \
180
+ const element = document.querySelector({selector_json}); \
181
+ if (!element) return 'not-found'; \
182
+ element.value = {text_json}; \
183
+ element.dispatchEvent(new Event('input', {{ bubbles: true }})); \
184
+ element.dispatchEvent(new Event('change', {{ bubbles: true }})); \
185
+ return 'typed'; \
186
+ }})()"
187
+ ),
188
+ "returnByValue": true
189
+ }),
190
+ )
191
+ .await
192
+ {
193
+ Ok(response) => match response["result"]["result"]["value"].as_str() {
194
+ Some("typed") => {
195
+ log_action(
196
+ "TYPE_SUCCESS",
197
+ &format!("Successfully typed into element '{selector}'"),
198
+ );
199
+ Ok("typed".into())
200
+ }
201
+ Some("not-found") => {
202
+ let err = format!("browser selector not found: {selector}");
203
+ log_action("TYPE_ERROR", &err);
204
+ Err(err)
205
+ }
206
+ _ => {
207
+ let err = response_error(&response);
208
+ log_action("TYPE_ERROR", &err);
209
+ Err(err)
210
+ }
211
+ },
212
+ Err(e) => {
213
+ log_action("TYPE_ERROR", &format!("Websocket error: {e}"));
214
+ Err(e)
215
+ }
216
+ }
217
+ }
218
+
219
+ pub async fn is_browser_running(config: &MintConfig) -> bool {
220
+ let endpoint = config
221
+ .extra
222
+ .get("browserDebugUrl")
223
+ .and_then(Value::as_str)
224
+ .unwrap_or("http://127.0.0.1:9222/json/list");
225
+ fetch_pages_endpoint(endpoint).await.is_ok()
226
+ }
227
+
228
+ pub async fn spawn_automation_browser(config: &MintConfig) -> Result<(), String> {
229
+ let endpoint = config
230
+ .extra
231
+ .get("browserDebugUrl")
232
+ .and_then(Value::as_str)
233
+ .unwrap_or("http://127.0.0.1:9222/json/list");
234
+
235
+ if fetch_pages_endpoint(endpoint).await.is_ok() {
236
+ return Ok(());
237
+ }
238
+
239
+ let browser_name = config
240
+ .extra
241
+ .get("automationBrowser")
242
+ .and_then(Value::as_str)
243
+ .unwrap_or("chromium");
244
+
245
+ let profile_dir = dirs::config_dir()
246
+ .unwrap_or_else(|| std::env::temp_dir())
247
+ .join("mint")
248
+ .join("automation-profile");
249
+ let profile_arg = format!("--user-data-dir={}", profile_dir.to_string_lossy());
250
+
251
+ let args = [
252
+ "--remote-debugging-port=9222".to_owned(),
253
+ "--no-first-run".to_owned(),
254
+ "--no-default-browser-check".to_owned(),
255
+ profile_arg,
256
+ ];
257
+
258
+ let mut spawned = false;
259
+ let executables = if cfg!(target_os = "windows") {
260
+ vec!["chrome.exe", "chromium.exe", "msedge.exe"]
261
+ } else if cfg!(target_os = "macos") {
262
+ vec![
263
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
264
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
265
+ "chromium",
266
+ "google-chrome",
267
+ ]
268
+ } else {
269
+ vec![
270
+ "chromium",
271
+ "google-chrome-stable",
272
+ "google-chrome",
273
+ "chrome",
274
+ "chromium-browser",
275
+ ]
276
+ };
277
+
278
+ for exe in executables {
279
+ if std::process::Command::new(exe)
280
+ .args(&args)
281
+ .stdout(std::process::Stdio::null())
282
+ .stderr(std::process::Stdio::null())
283
+ .spawn()
284
+ .is_ok()
285
+ {
286
+ spawned = true;
287
+ break;
288
+ }
289
+ }
290
+
291
+ if !spawned {
292
+ return Err(format!(
293
+ "Could not find or spawn browser '{browser_name}' with remote debugging. Please verify it is installed."
294
+ ));
295
+ }
296
+
297
+ for _ in 0..20 {
298
+ tokio::time::sleep(std::time::Duration::from_millis(250)).await;
299
+ if fetch_pages_endpoint(endpoint).await.is_ok() {
300
+ return Ok(());
301
+ }
302
+ }
303
+
304
+ Err("Browser spawned but remote debugging port 9222 did not become available.".to_string())
305
+ }
306
+
307
+ pub async fn ensure_page_open(config: &MintConfig) -> Result<(), String> {
308
+ if !is_browser_running(config).await {
309
+ return Err("Browser automation is not running. Please run 'mint auto' first.".to_string());
310
+ }
311
+ let endpoint = config
312
+ .extra
313
+ .get("browserDebugUrl")
314
+ .and_then(Value::as_str)
315
+ .unwrap_or("http://127.0.0.1:9222/json/list");
316
+
317
+ let pages = fetch_pages_endpoint(endpoint).await?;
318
+ let has_page = pages.iter().any(|p| p["type"] == "page");
319
+ if !has_page {
320
+ let base_url = endpoint.replace("/json/list", "/json/new");
321
+ let client = crate::HTTP_CLIENT.clone();
322
+ let _ = client
323
+ .put(&base_url)
324
+ .send()
325
+ .await
326
+ .map_err(|e| format!("failed to open new tab: {e}"))?;
327
+ }
328
+ Ok(())
329
+ }
330
+
331
+ async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
332
+ let page = fetch_pages(config)
333
+ .await?
334
+ .into_iter()
335
+ .find(|page| page["type"] == "page")
336
+ .ok_or("Chrome DevTools did not report an open browser page")?;
337
+ let socket_url = page["webSocketDebuggerUrl"]
338
+ .as_str()
339
+ .ok_or("Chrome DevTools page does not expose a websocket URL")?;
340
+ let (mut socket, _) = connect_async(socket_url)
341
+ .await
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
+
398
+ socket
399
+ .send(Message::Text(
400
+ json!({ "id": 1, "method": method, "params": params })
401
+ .to_string()
402
+ .into(),
403
+ ))
404
+ .await
405
+ .map_err(|error| error.to_string())?;
406
+ while let Some(message) = socket.next().await {
407
+ let message = message.map_err(|error| error.to_string())?;
408
+ let Message::Text(raw) = message else {
409
+ continue;
410
+ };
411
+ let value: Value = serde_json::from_str(&raw).map_err(|error| error.to_string())?;
412
+ if value["id"] == 1 {
413
+ return Ok(value);
414
+ }
415
+ }
416
+ Err("Chrome DevTools websocket closed before returning a response".into())
417
+ }
418
+
419
+ async fn fetch_pages(config: &MintConfig) -> Result<Vec<Value>, String> {
420
+ let endpoint = config
421
+ .extra
422
+ .get("browserDebugUrl")
423
+ .and_then(Value::as_str)
424
+ .unwrap_or("http://127.0.0.1:9222/json/list");
425
+ fetch_pages_endpoint(endpoint).await
426
+ }
427
+
428
+ async fn fetch_pages_endpoint(endpoint: &str) -> Result<Vec<Value>, String> {
429
+ let client = reqwest::Client::builder()
430
+ .timeout(std::time::Duration::from_millis(1000))
431
+ .connect_timeout(std::time::Duration::from_millis(500))
432
+ .build()
433
+ .unwrap_or_else(|_| crate::HTTP_CLIENT.clone());
434
+
435
+ let value: Value = client
436
+ .get(endpoint)
437
+ .send()
438
+ .await
439
+ .map_err(|error| format!("unable to reach Chrome DevTools at {endpoint}: {error}"))?
440
+ .error_for_status()
441
+ .map_err(|error| error.to_string())?
442
+ .json()
443
+ .await
444
+ .map_err(|error| error.to_string())?;
445
+ value
446
+ .as_array()
447
+ .cloned()
448
+ .ok_or_else(|| "Chrome DevTools response was not a page list".into())
449
+ }
450
+
451
+ fn response_error(response: &Value) -> String {
452
+ response["error"]["message"]
453
+ .as_str()
454
+ .unwrap_or("Chrome DevTools returned an unexpected response")
455
+ .to_owned()
456
+ }
@@ -194,6 +194,7 @@ async fn answer(text: &str, system_instruction: &str) -> String {
194
194
  audio_data_uri: None,
195
195
  document_attachment: None,
196
196
  workspace_path: None,
197
+ agent_id: None,
197
198
  },
198
199
  )
199
200
  .await
@@ -99,6 +99,8 @@ pub struct ChatRequest {
99
99
  pub document_attachment: Option<DocumentAttachment>,
100
100
  #[serde(default)]
101
101
  pub workspace_path: Option<String>,
102
+ #[serde(default)]
103
+ pub agent_id: Option<String>,
102
104
  }
103
105
 
104
106
  #[derive(Debug, Clone, Deserialize)]
@@ -965,6 +967,7 @@ mod tests {
965
967
  audio_data_uri: None,
966
968
  document_attachment: None,
967
969
  workspace_path: None,
970
+ agent_id: None,
968
971
  })
969
972
  .unwrap();
970
973
  assert_eq!(parts.len(), 3);