@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.
- package/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +20 -6
- package/Release_Note.md +129 -0
- package/assets/Mint_CLI.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +242 -54
- package/crates/mint-cli/src/main.rs +397 -74
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +64 -2
- package/crates/mint-core/src/browser.rs +456 -0
- package/crates/mint-core/src/channels.rs +1 -0
- package/crates/mint-core/src/chat.rs +3 -0
- package/crates/mint-core/src/config.rs +130 -1
- package/crates/mint-core/src/lib.rs +19 -0
- package/crates/mint-core/src/orchestration.rs +317 -12
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/tts.rs +3 -1
- package/crates/mint-core/src/workflows.rs +1 -22
- package/flatpak/com.pheem49.mint.desktop +9 -0
- package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
- package/flatpak/com.pheem49.mint.yaml +52 -0
- package/package.json +1 -1
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/shared-modules/CODEOWNERS +23 -0
- package/shared-modules/README.md +79 -0
- package/shared-modules/SDL/README.md +3 -0
- package/shared-modules/SDL/SDL-1.2.15.json +51 -0
- package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
- package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
- package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
- package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
- package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
- package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
- package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
- package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
- package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
- package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
- package/shared-modules/SDL/sdl12-compat.json +22 -0
- package/shared-modules/SDL2/SDL2_image.json +27 -0
- package/shared-modules/SDL2/SDL2_ttf.json +29 -0
- package/shared-modules/cld2/CMakeLists.txt +157 -0
- package/shared-modules/cld2/cld2.json +24 -0
- package/shared-modules/clutter/clutter.json +72 -0
- package/shared-modules/dbus-glib/dbus-glib.json +29 -0
- package/shared-modules/glew/glew.json +36 -0
- package/shared-modules/glu/glu-9.json +12 -0
- package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
- package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
- package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
- package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
- package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
- package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
- package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
- package/shared-modules/gtk2/gtk2.json +120 -0
- package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
- package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
- package/shared-modules/gtk2/readme.md +10 -0
- package/shared-modules/intltool/intltool-0.51.json +20 -0
- package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
- package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
- package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
- package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
- package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
- package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
- package/shared-modules/lame/lame-3.99.5.json +32 -0
- package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
- package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
- package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
- package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
- package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
- package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
- package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
- package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
- package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
- package/shared-modules/libcanberra/libcanberra.json +44 -0
- package/shared-modules/libmad/libmad.json +19 -0
- package/shared-modules/libsecret/libsecret.json +23 -0
- package/shared-modules/libsoup/libsoup-2.4.json +14 -0
- package/shared-modules/libusb/libusb.json +27 -0
- package/shared-modules/linux-audio/dssi.json +31 -0
- package/shared-modules/linux-audio/fftw3f-static.json +44 -0
- package/shared-modules/linux-audio/fftw3f.json +42 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
- package/shared-modules/linux-audio/fluidsynth2.json +19 -0
- package/shared-modules/linux-audio/ladspa.json +26 -0
- package/shared-modules/linux-audio/lash.json +35 -0
- package/shared-modules/linux-audio/libinstpatch.json +16 -0
- package/shared-modules/linux-audio/liblo-static.json +22 -0
- package/shared-modules/linux-audio/liblo.json +17 -0
- package/shared-modules/linux-audio/lilv-static.json +94 -0
- package/shared-modules/linux-audio/lilv.json +82 -0
- package/shared-modules/linux-audio/lrdf.json +57 -0
- package/shared-modules/linux-audio/lv2.json +21 -0
- package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
- package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
- package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
- package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
- package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
- package/shared-modules/linux-audio/stk.json +28 -0
- package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
- package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
- package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
- package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
- package/shared-modules/lua5.3/lua.pc.in +8 -0
- package/shared-modules/lua5.3/luaconf.patch +47 -0
- package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
- package/shared-modules/lua5.4/lua-5.4.json +41 -0
- package/shared-modules/lua5.4/lua.pc.in +8 -0
- package/shared-modules/lua5.4/luaconf.patch +34 -0
- package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
- package/shared-modules/luajit/luajit.json +30 -0
- package/shared-modules/lzo/lzo.json +27 -0
- package/shared-modules/mac/mac.json +18 -0
- package/shared-modules/physfs/physfs.json +26 -0
- package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
- package/shared-modules/pygame/cython-fixes.patch +34 -0
- package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
- package/shared-modules/pygame/glib.patch +25 -0
- package/shared-modules/pygame/pygame1-python2.json +157 -0
- package/shared-modules/pygtk/pygtk-c99.patch +15 -0
- package/shared-modules/pygtk/pygtk.json +117 -0
- package/shared-modules/pygtk/pygtk.patch +39 -0
- package/shared-modules/python2.7/python-2.7.json +48 -0
- package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
- package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
- package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
- package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
- package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
- package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
- package/shared-modules/update-checker.sh +50 -0
- package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
- package/shared-modules/vorbisgain/0002-errno.patch +33 -0
- package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
- package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
- package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
- package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
- package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
- package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
- package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
- package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
- package/shared-modules/vorbisgain/README.md +23 -0
- package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
- package/src/renderer/shared/agentProgress.ts +145 -0
- package/src/renderer/shared/calculator.ts +52 -0
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
- package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
- package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
- package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
- package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
- package/src/renderer/shared/constants/models.ts +91 -0
- package/src/renderer/shared/platform.ts +82 -0
- package/src/renderer/shared/types.ts +140 -0
- package/src/renderer/shared/utils/agentActivity.ts +187 -0
- package/src/renderer/shared/utils/approval.ts +1 -0
- package/src/renderer/shared/utils/approval.tsx +64 -0
- package/src/renderer/shared/utils/markdown.tsx +347 -0
- package/src/renderer/shared/utils/providers.ts +36 -0
- package/src/renderer/shared/utils/speech.ts +216 -0
- package/src/renderer/shared/utils/ui.ts +147 -0
- package/src/renderer/src/agentProgress.ts +11 -39
- package/src/renderer/src/calculator.ts +3 -46
- package/src/renderer/src/components/ChatPanel.tsx +230 -1202
- package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src/components/MintDashboard.tsx +63 -171
- package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
- package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +6 -0
- package/src/renderer/src/css/styles.css +148 -11
- package/src/renderer/src/index.css +12 -1
- package/src/renderer/src/tauri.ts +169 -143
- package/src/renderer/src-web/agentProgress.ts +11 -39
- package/src/renderer/src-web/calculator.ts +3 -46
- package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
- package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src-web/css/image-studio.css +6 -0
- package/src/renderer/src-web/css/styles.css +184 -12
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +204 -104
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +254 -59
- package/src-tauri/src/webhooks.rs +1 -0
- package/src-tauri/src/workflows.rs +57 -4
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -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);
|