@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
@@ -1,141 +1 @@
1
- use futures_util::{SinkExt, StreamExt};
2
- use mint_core::MintConfig;
3
- use serde::Serialize;
4
- use serde_json::{Value, json};
5
- use tokio_tungstenite::{connect_async, tungstenite::Message};
6
-
7
- #[derive(Debug, Clone, Serialize)]
8
- #[serde(rename_all = "camelCase")]
9
- pub struct BrowserTab {
10
- pub id: String,
11
- pub title: String,
12
- pub url: String,
13
- }
14
-
15
- pub async fn list_tabs(config: &MintConfig) -> Result<Vec<BrowserTab>, String> {
16
- Ok(fetch_pages(config)
17
- .await?
18
- .into_iter()
19
- .filter_map(|page| {
20
- Some(BrowserTab {
21
- id: page["id"].as_str()?.to_owned(),
22
- title: page["title"].as_str().unwrap_or_default().to_owned(),
23
- url: page["url"].as_str().unwrap_or_default().to_owned(),
24
- })
25
- })
26
- .collect())
27
- }
28
-
29
- pub async fn navigate(config: &MintConfig, url: &str) -> Result<String, String> {
30
- if !(url.starts_with("https://") || url.starts_with("http://")) {
31
- return Err("browser navigation only supports http and https URLs".into());
32
- }
33
- let response = cdp_call(config, "Page.navigate", json!({ "url": url })).await?;
34
- response["result"]["frameId"]
35
- .as_str()
36
- .map(|_| format!("navigating to {url}"))
37
- .ok_or_else(|| response_error(&response))
38
- }
39
-
40
- pub async fn read_page_text(config: &MintConfig) -> Result<String, String> {
41
- let response = cdp_call(
42
- config,
43
- "Runtime.evaluate",
44
- json!({
45
- "expression": "document.body ? document.body.innerText.substring(0, 12000) : ''",
46
- "returnByValue": true
47
- }),
48
- )
49
- .await?;
50
- response["result"]["result"]["value"]
51
- .as_str()
52
- .map(str::to_owned)
53
- .ok_or_else(|| response_error(&response))
54
- }
55
-
56
- pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String> {
57
- let selector = selector.trim();
58
- if selector.is_empty() || selector.len() > 500 {
59
- return Err("browser selector must contain between 1 and 500 characters".into());
60
- }
61
- let selector = serde_json::to_string(selector).map_err(|error| error.to_string())?;
62
- let response = cdp_call(
63
- config,
64
- "Runtime.evaluate",
65
- json!({
66
- "expression": format!(
67
- "(() => {{ const element = document.querySelector({selector}); if (!element) return 'not-found'; element.click(); return 'clicked'; }})()"
68
- ),
69
- "returnByValue": true
70
- }),
71
- )
72
- .await?;
73
- match response["result"]["result"]["value"].as_str() {
74
- Some("clicked") => Ok("clicked".into()),
75
- Some("not-found") => Err(format!("browser selector not found: {selector}")),
76
- _ => Err(response_error(&response)),
77
- }
78
- }
79
-
80
- async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
81
- let page = fetch_pages(config)
82
- .await?
83
- .into_iter()
84
- .find(|page| page["type"] == "page")
85
- .ok_or("Chrome DevTools did not report an open browser page")?;
86
- let socket_url = page["webSocketDebuggerUrl"]
87
- .as_str()
88
- .ok_or("Chrome DevTools page does not expose a websocket URL")?;
89
- let (mut socket, _) = connect_async(socket_url)
90
- .await
91
- .map_err(|error| format!("unable to connect to Chrome DevTools websocket: {error}"))?;
92
- socket
93
- .send(Message::Text(
94
- json!({ "id": 1, "method": method, "params": params })
95
- .to_string()
96
- .into(),
97
- ))
98
- .await
99
- .map_err(|error| error.to_string())?;
100
- while let Some(message) = socket.next().await {
101
- let message = message.map_err(|error| error.to_string())?;
102
- let Message::Text(raw) = message else {
103
- continue;
104
- };
105
- let value: Value = serde_json::from_str(&raw).map_err(|error| error.to_string())?;
106
- if value["id"] == 1 {
107
- return Ok(value);
108
- }
109
- }
110
- Err("Chrome DevTools websocket closed before returning a response".into())
111
- }
112
-
113
- async fn fetch_pages(config: &MintConfig) -> Result<Vec<Value>, String> {
114
- let endpoint = config
115
- .extra
116
- .get("browserDebugUrl")
117
- .and_then(Value::as_str)
118
- .unwrap_or("http://127.0.0.1:9222/json/list");
119
- let value: Value = mint_core::HTTP_CLIENT
120
- .clone()
121
- .get(endpoint)
122
- .send()
123
- .await
124
- .map_err(|error| format!("unable to reach Chrome DevTools at {endpoint}: {error}"))?
125
- .error_for_status()
126
- .map_err(|error| error.to_string())?
127
- .json()
128
- .await
129
- .map_err(|error| error.to_string())?;
130
- value
131
- .as_array()
132
- .cloned()
133
- .ok_or_else(|| "Chrome DevTools response was not a page list".into())
134
- }
135
-
136
- fn response_error(response: &Value) -> String {
137
- response["error"]["message"]
138
- .as_str()
139
- .unwrap_or("Chrome DevTools returned an unexpected response")
140
- .to_owned()
141
- }
1
+ // Deleted - Moved browser automation functions to mint-core.
@@ -8,11 +8,11 @@ use serde::Deserialize;
8
8
  use serde_json::{Value, json};
9
9
  use tauri::{AppHandle, Emitter, Manager};
10
10
 
11
- use crate::browser::{click, navigate, read_page_text};
11
+ use mint_core::browser::{click, navigate, read_page_text, type_text};
12
12
 
13
13
  const MAX_STEPS: usize = 20;
14
14
  const SYSTEM_PROMPT: &str = r#"You are Mint's native background task agent. Return only JSON:
15
- {"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
15
+ {"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|browser_type|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
16
16
  Use only one action per response. Background tasks never mutate the filesystem or execute shell commands. Use propose_folder, propose_write_file, and propose_bash so Mint can record a proposal for explicit user approval."#;
17
17
 
18
18
  #[derive(Debug, Deserialize)]
@@ -177,6 +177,7 @@ async fn execute_action(
177
177
  "open_url" => navigate(config, &action.target).await,
178
178
  "browser_read" => read_page_text(config).await,
179
179
  "browser_click" => click(config, &action.target).await,
180
+ "browser_type" => type_text(config, &action.target, &action.data).await,
180
181
  "knowledge_search" => {
181
182
  let hits = KnowledgeStore::open_default()
182
183
  .map_err(|error| error.to_string())?
@@ -1,4 +1,3 @@
1
- mod browser;
2
1
  mod desktop;
3
2
  mod discord_rpc;
4
3
  mod events;
@@ -11,7 +10,7 @@ mod updater;
11
10
  mod webhooks;
12
11
  mod workflows;
13
12
 
14
- use browser::{
13
+ use mint_core::browser::{
15
14
  BrowserTab, click as browser_click, list_tabs as browser_list_tabs,
16
15
  navigate as browser_navigate, read_page_text,
17
16
  };
@@ -41,7 +40,7 @@ use mint_core::{
41
40
  use plugins::execute_plugin;
42
41
 
43
42
  pub struct ApprovalsState {
44
- pub pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
43
+ pub pending: Mutex<HashMap<String, oneshot::Sender<ApprovalOutcome>>>,
45
44
  }
46
45
 
47
46
  static COUNTER: AtomicU64 = AtomicU64::new(1);
@@ -136,6 +135,26 @@ async fn get_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry,
136
135
  .map_err(|error| format!("workspace tree task failed: {error}"))?
137
136
  }
138
137
 
138
+ #[tauri::command]
139
+ async fn create_workspace_file(path: String) -> Result<(), String> {
140
+ std::fs::write(&path, "").map_err(|error| error.to_string())
141
+ }
142
+
143
+ #[tauri::command]
144
+ async fn create_workspace_folder(path: String) -> Result<(), String> {
145
+ std::fs::create_dir_all(&path).map_err(|error| error.to_string())
146
+ }
147
+
148
+ #[tauri::command]
149
+ async fn delete_workspace_item(path: String) -> Result<(), String> {
150
+ let path_buf = std::path::PathBuf::from(path);
151
+ if path_buf.is_dir() {
152
+ std::fs::remove_dir_all(path_buf).map_err(|error| error.to_string())
153
+ } else {
154
+ std::fs::remove_file(path_buf).map_err(|error| error.to_string())
155
+ }
156
+ }
157
+
139
158
  fn build_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry, String> {
140
159
  let root = workspace_root(path.as_deref())?;
141
160
  let name = root
@@ -191,6 +210,11 @@ fn workspace_root(path: Option<&str>) -> Result<PathBuf, String> {
191
210
  None => std::env::current_dir().map_err(|error| error.to_string())?,
192
211
  };
193
212
  let root = root.canonicalize().map_err(|error| error.to_string())?;
213
+ let root = if root.ends_with("src-tauri") {
214
+ root.parent().unwrap_or(&root).to_path_buf()
215
+ } else {
216
+ root
217
+ };
194
218
  if !root.is_dir() {
195
219
  return Err(format!("workspace is not a directory: {}", root.display()));
196
220
  }
@@ -298,10 +322,30 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
298
322
  if request.message.starts_with("/chat ") {
299
323
  let mut clean_request = request.clone();
300
324
  clean_request.message = request.message.strip_prefix("/chat ").unwrap().to_owned();
325
+ let config_clone = config.clone();
326
+ let chat_id_str = request.chat_id.clone().unwrap_or_default();
301
327
 
302
- let (response, _) = orchestrate_chat_with_fallback(&config, &clean_request)
303
- .await
304
- .map_err(|error| error.to_string())?;
328
+ let join_handle = tokio::spawn(async move {
329
+ orchestrate_chat_with_fallback(&config_clone, &clean_request).await
330
+ });
331
+
332
+ let abort_handle = join_handle.abort_handle();
333
+ if !chat_id_str.is_empty() {
334
+ mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
335
+ }
336
+
337
+ let res = join_handle.await;
338
+
339
+ if !chat_id_str.is_empty() {
340
+ mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
341
+ }
342
+
343
+ let (response, _) = match res {
344
+ Ok(Ok(val)) => val,
345
+ Ok(Err(e)) => return Err(e.to_string()),
346
+ Err(e) if e.is_cancelled() => return Err("Chat execution cancelled by user".to_string()),
347
+ Err(e) => return Err(format!("Task panicked: {}", e)),
348
+ };
305
349
  return Ok(response);
306
350
  }
307
351
 
@@ -330,33 +374,56 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
330
374
  )
331
375
  .map_err(|e| e.to_string())?;
332
376
 
333
- let approved =
377
+ let outcome =
334
378
  tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
335
- .unwrap_or(false);
336
-
337
- if approved {
338
- Ok(ApprovalOutcome::Approved)
339
- } else {
340
- Ok(ApprovalOutcome::Denied)
341
- }
379
+ .unwrap_or(ApprovalOutcome::Denied);
380
+ Ok(outcome)
342
381
  };
343
382
 
344
383
  let progress_cb = |_| {};
345
384
  let on_chunk = |_| {};
346
385
 
347
- let res = orchestrate_agent_loop(
348
- &config,
349
- &request.message,
350
- &root,
351
- request.image_data_uri.clone(),
352
- request.chat_id.as_deref(),
353
- fast_mode,
354
- approve_cb,
355
- progress_cb,
356
- on_chunk,
357
- )
358
- .await
359
- .map_err(|e| e.to_string())?;
386
+ let chat_id_str = request.chat_id.clone().unwrap_or_default();
387
+ let config_clone = config.clone();
388
+ let message_clone = request.message.clone();
389
+ let root_clone = root.clone();
390
+ let image_data_uri_clone = request.image_data_uri.clone();
391
+ let chat_id_clone = request.chat_id.clone();
392
+ let agent_id_clone = request.agent_id.clone();
393
+
394
+ let join_handle = tokio::spawn(async move {
395
+ orchestrate_agent_loop(
396
+ &config_clone,
397
+ &message_clone,
398
+ &root_clone,
399
+ image_data_uri_clone,
400
+ chat_id_clone.as_deref(),
401
+ agent_id_clone.as_deref(),
402
+ fast_mode,
403
+ approve_cb,
404
+ progress_cb,
405
+ on_chunk,
406
+ )
407
+ .await
408
+ });
409
+
410
+ let abort_handle = join_handle.abort_handle();
411
+ if !chat_id_str.is_empty() {
412
+ mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
413
+ }
414
+
415
+ let res = join_handle.await;
416
+
417
+ if !chat_id_str.is_empty() {
418
+ mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
419
+ }
420
+
421
+ let res = match res {
422
+ Ok(Ok(val)) => val,
423
+ Ok(Err(e)) => return Err(e.to_string()),
424
+ Err(e) if e.is_cancelled() => return Err("Agent execution cancelled by user".to_string()),
425
+ Err(e) => return Err(format!("Task panicked: {}", e)),
426
+ };
360
427
 
361
428
  Ok(ChatResponse {
362
429
  provider: res.provider,
@@ -378,13 +445,34 @@ async fn stream_chat_message(
378
445
  if request.message.starts_with("/chat ") {
379
446
  let mut clean_request = request.clone();
380
447
  clean_request.message = request.message.strip_prefix("/chat ").unwrap().to_owned();
448
+ let config_clone = config.clone();
449
+ let on_event_clone = on_event.clone();
450
+ let chat_id_str = request.chat_id.clone().unwrap_or_default();
381
451
 
382
- let (response, _) =
383
- orchestrate_chat_stream_with_fallback(&config, &clean_request, |chunk| {
384
- let _ = on_event.send(DesktopStreamEvent::Chunk { chunk });
452
+ let join_handle = tokio::spawn(async move {
453
+ orchestrate_chat_stream_with_fallback(&config_clone, &clean_request, move |chunk| {
454
+ let _ = on_event_clone.send(DesktopStreamEvent::Chunk { chunk });
385
455
  })
386
456
  .await
387
- .map_err(|error| error.to_string())?;
457
+ });
458
+
459
+ let abort_handle = join_handle.abort_handle();
460
+ if !chat_id_str.is_empty() {
461
+ mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
462
+ }
463
+
464
+ let res = join_handle.await;
465
+
466
+ if !chat_id_str.is_empty() {
467
+ mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
468
+ }
469
+
470
+ let (response, _) = match res {
471
+ Ok(Ok(val)) => val,
472
+ Ok(Err(e)) => return Err(e.to_string()),
473
+ Err(e) if e.is_cancelled() => return Err("Chat execution cancelled by user".to_string()),
474
+ Err(e) => return Err(format!("Task panicked: {}", e)),
475
+ };
388
476
  return Ok(response);
389
477
  }
390
478
 
@@ -413,15 +501,10 @@ async fn stream_chat_message(
413
501
  )
414
502
  .map_err(|e| e.to_string())?;
415
503
 
416
- let approved =
504
+ let outcome =
417
505
  tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
418
- .unwrap_or(false);
419
-
420
- if approved {
421
- Ok(ApprovalOutcome::Approved)
422
- } else {
423
- Ok(ApprovalOutcome::Denied)
424
- }
506
+ .unwrap_or(ApprovalOutcome::Denied);
507
+ Ok(outcome)
425
508
  };
426
509
 
427
510
  let on_progress_event = on_event.clone();
@@ -442,19 +525,47 @@ async fn stream_chat_message(
442
525
  }
443
526
  };
444
527
 
445
- let res = orchestrate_agent_loop(
446
- &config,
447
- &request.message,
448
- &root,
449
- request.image_data_uri.clone(),
450
- request.chat_id.as_deref(),
451
- fast_mode,
452
- approve_cb,
453
- progress_cb,
454
- on_chunk,
455
- )
456
- .await
457
- .map_err(|e| e.to_string())?;
528
+ let chat_id_str = request.chat_id.clone().unwrap_or_default();
529
+ let config_clone = config.clone();
530
+ let message_clone = request.message.clone();
531
+ let root_clone = root.clone();
532
+ let image_data_uri_clone = request.image_data_uri.clone();
533
+ let chat_id_clone = request.chat_id.clone();
534
+ let agent_id_clone = request.agent_id.clone();
535
+
536
+ let join_handle = tokio::spawn(async move {
537
+ orchestrate_agent_loop(
538
+ &config_clone,
539
+ &message_clone,
540
+ &root_clone,
541
+ image_data_uri_clone,
542
+ chat_id_clone.as_deref(),
543
+ agent_id_clone.as_deref(),
544
+ fast_mode,
545
+ approve_cb,
546
+ progress_cb,
547
+ on_chunk,
548
+ )
549
+ .await
550
+ });
551
+
552
+ let abort_handle = join_handle.abort_handle();
553
+ if !chat_id_str.is_empty() {
554
+ mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
555
+ }
556
+
557
+ let res = join_handle.await;
558
+
559
+ if !chat_id_str.is_empty() {
560
+ mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
561
+ }
562
+
563
+ let res = match res {
564
+ Ok(Ok(val)) => val,
565
+ Ok(Err(e)) => return Err(e.to_string()),
566
+ Err(e) if e.is_cancelled() => return Err("Agent execution cancelled by user".to_string()),
567
+ Err(e) => return Err(format!("Task panicked: {}", e)),
568
+ };
458
569
 
459
570
  Ok(ChatResponse {
460
571
  provider: res.provider,
@@ -464,6 +575,12 @@ async fn stream_chat_message(
464
575
  })
465
576
  }
466
577
 
578
+ #[tauri::command]
579
+ async fn cancel_chat_message(chat_id: String) -> Result<(), String> {
580
+ mint_core::cancel_agent(&chat_id);
581
+ Ok(())
582
+ }
583
+
467
584
  #[tauri::command]
468
585
  fn save_interaction_agent_activity(
469
586
  interaction_id: i64,
@@ -553,20 +670,52 @@ struct LearnedSkillDto {
553
670
  source_path: String,
554
671
  content: String,
555
672
  updated_at: String,
673
+ location: String,
556
674
  }
557
675
 
558
676
  #[tauri::command]
559
- fn list_learned_skills() -> Result<Vec<LearnedSkillDto>, String> {
677
+ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkillDto>, String> {
560
678
  let store = MemoryStore::open_default().map_err(|e| e.to_string())?;
561
- let skills = store.learned_skills(100).map_err(|e| e.to_string())?;
562
- let dtos = skills
563
- .into_iter()
564
- .map(|s| LearnedSkillDto {
679
+ let db_skills = store.learned_skills(100).map_err(|e| e.to_string())?;
680
+
681
+ let mut global_skills = Vec::new();
682
+ if let Some(home) = dirs::home_dir() {
683
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
684
+ if !global_skills_path.exists() {
685
+ let _ = std::fs::create_dir_all(&global_skills_path);
686
+ }
687
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut global_skills);
688
+ }
689
+
690
+ let mut workspace_skills = Vec::new();
691
+ if let Ok(root) = workspace_root(workspace_path.as_deref()) {
692
+ let workspace_skills_path1 = root.join(".agents").join("skills");
693
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut workspace_skills);
694
+
695
+ let workspace_skills_path2 = root.join("skills");
696
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut workspace_skills);
697
+ }
698
+
699
+ let mut unique_skills = std::collections::BTreeMap::new();
700
+ for s in db_skills {
701
+ unique_skills.insert(s.name.clone(), (s, "database"));
702
+ }
703
+ for s in global_skills {
704
+ unique_skills.insert(s.name.clone(), (s, "global"));
705
+ }
706
+ for s in workspace_skills {
707
+ unique_skills.insert(s.name.clone(), (s, "workspace"));
708
+ }
709
+
710
+ let dtos = unique_skills
711
+ .into_values()
712
+ .map(|(s, loc)| LearnedSkillDto {
565
713
  id: s.id,
566
714
  name: s.name,
567
715
  source_path: s.source_path,
568
716
  content: s.content,
569
717
  updated_at: s.created_at,
718
+ location: loc.to_string(),
570
719
  })
571
720
  .collect();
572
721
  Ok(dtos)
@@ -584,6 +733,7 @@ fn add_learned_skill(name: String, content: String) -> Result<LearnedSkillDto, S
584
733
  source_path: skill.source_path,
585
734
  content: skill.content,
586
735
  updated_at: skill.created_at,
736
+ location: "database".to_string(),
587
737
  })
588
738
  }
589
739
 
@@ -611,16 +761,47 @@ async fn submit_tool_approval(
611
761
  state: tauri::State<'_, ApprovalsState>,
612
762
  token: String,
613
763
  approved: bool,
764
+ answer: Option<String>,
614
765
  ) -> Result<(), String> {
615
766
  let mut pending = state.pending.lock().map_err(|error| error.to_string())?;
616
767
  if let Some(tx) = pending.remove(&token) {
617
- let _ = tx.send(approved);
768
+ let outcome = if let Some(ans) = answer {
769
+ if !ans.trim().is_empty() {
770
+ ApprovalOutcome::Intercepted(ans)
771
+ } else {
772
+ ApprovalOutcome::Denied
773
+ }
774
+ } else if approved {
775
+ ApprovalOutcome::Approved
776
+ } else {
777
+ ApprovalOutcome::Denied
778
+ };
779
+ let _ = tx.send(outcome);
618
780
  Ok(())
619
781
  } else {
620
782
  Err("No pending approval found for this token".into())
621
783
  }
622
784
  }
623
785
 
786
+ #[derive(serde::Serialize)]
787
+ #[serde(rename_all = "camelCase")]
788
+ pub struct DetectedTools {
789
+ pub docker: bool,
790
+ pub git: bool,
791
+ pub gh: bool,
792
+ pub node: bool,
793
+ }
794
+
795
+ #[tauri::command]
796
+ async fn detect_system_tools() -> Result<DetectedTools, String> {
797
+ Ok(DetectedTools {
798
+ docker: mint_core::config::which("docker"),
799
+ git: mint_core::config::which("git"),
800
+ gh: mint_core::config::which("gh"),
801
+ node: mint_core::config::which("node"),
802
+ })
803
+ }
804
+
624
805
  #[derive(serde::Serialize)]
625
806
  #[serde(rename_all = "camelCase")]
626
807
  struct DesktopImageGenResponse {
@@ -839,7 +1020,15 @@ async fn click_browser_selector(selector: String) -> Result<String, String> {
839
1020
  )
840
1021
  .await
841
1022
  }
842
-
1023
+ #[tauri::command]
1024
+ async fn type_in_browser(selector: String, text: String) -> Result<String, String> {
1025
+ mint_core::browser::type_text(
1026
+ &load_config().map_err(|error| error.to_string())?,
1027
+ &selector,
1028
+ &text,
1029
+ )
1030
+ .await
1031
+ }
843
1032
  #[tauri::command]
844
1033
  fn start_screen_capture(app: AppHandle) -> Result<(), String> {
845
1034
  open_desktop_window(&app, "screen-picker")
@@ -1022,7 +1211,11 @@ pub fn run() {
1022
1211
  })
1023
1212
  .invoke_handler(tauri::generate_handler![
1024
1213
  get_runtime_status,
1214
+ detect_system_tools,
1025
1215
  get_workspace_tree,
1216
+ create_workspace_file,
1217
+ create_workspace_folder,
1218
+ delete_workspace_item,
1026
1219
  generate_images,
1027
1220
  select_workspace_directory,
1028
1221
  get_config,
@@ -1033,6 +1226,7 @@ pub fn run() {
1033
1226
  inspect_shell_command,
1034
1227
  send_chat_message,
1035
1228
  stream_chat_message,
1229
+ cancel_chat_message,
1036
1230
  submit_tool_approval,
1037
1231
  get_recent_interactions,
1038
1232
  save_interaction_agent_activity,
@@ -1067,6 +1261,7 @@ pub fn run() {
1067
1261
  navigate_browser,
1068
1262
  read_browser_page,
1069
1263
  click_browser_selector,
1264
+ type_in_browser,
1070
1265
  start_screen_capture,
1071
1266
  submit_screen_selection,
1072
1267
  submit_spotlight,
@@ -154,6 +154,7 @@ async fn answer(text: &str, system: &str) -> String {
154
154
  audio_data_uri: None,
155
155
  document_attachment: None,
156
156
  workspace_path: None,
157
+ agent_id: None,
157
158
  },
158
159
  )
159
160
  .await